From 08870e0b4ab67de92d8dc7687840963dcefd1125 Mon Sep 17 00:00:00 2001 From: KonshinHaoshin Date: Sun, 8 Mar 2026 00:33:00 +0800 Subject: [PATCH 1/3] fix emoji display --- .../webgal/src/Stage/TextBox/IMSSTextbox.tsx | 59 +++++++++++++++---- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/packages/webgal/src/Stage/TextBox/IMSSTextbox.tsx b/packages/webgal/src/Stage/TextBox/IMSSTextbox.tsx index f603f2792..86e411eff 100644 --- a/packages/webgal/src/Stage/TextBox/IMSSTextbox.tsx +++ b/packages/webgal/src/Stage/TextBox/IMSSTextbox.tsx @@ -1,5 +1,5 @@ import styles from './textbox.module.scss'; -import { useEffect } from 'react'; +import { isValidElement, ReactNode, useEffect } from 'react'; import { WebGAL } from '@/Core/WebGAL'; import { ITextboxProps } from './types'; import useApplyStyle from '@/hooks/useApplyStyle'; @@ -8,6 +8,26 @@ import { textSize } from '@/store/userDataInterface'; import { useSelector } from 'react-redux'; import { RootState } from '@/store/store'; +const EMOJI_REGEX = /\p{Extended_Pictographic}|\p{Emoji_Presentation}/u; + +function getNodeTextContent(node: ReactNode): string { + if (typeof node === 'string' || typeof node === 'number') { + return String(node); + } + if (Array.isArray(node)) { + return node.map((item) => getNodeTextContent(item)).join(''); + } + if (isValidElement(node)) { + return getNodeTextContent(node.props.children); + } + return ''; +} + +function shouldRenderPlainText(node: ReactNode): boolean { + const text = getNodeTextContent(node); + return EMOJI_REGEX.test(text); +} + export default function IMSSTextbox(props: ITextboxProps) { const { textArray, @@ -72,6 +92,13 @@ export default function IMSSTextbox(props: ITextboxProps) { } const styleClassName = ' ' + css(style, { label: 'showname' }); const styleAllText = ' ' + css(style_alltext, { label: 'showname' }); + if (shouldRenderPlainText(e)) { + return ( + + {e} + + ); + } if (isEnhanced) { return ( @@ -140,6 +167,8 @@ export default function IMSSTextbox(props: ITextboxProps) { } const styleClassName = ' ' + css(style); const styleAllText = ' ' + css(style_alltext); + const isPlainText = shouldRenderPlainText(e); + if (allTextIndex < prevLength) { return ( - - {e} - {e} - {isUseStroke && {e}} - + {isPlainText ? ( + {e} + ) : ( + + {e} + {e} + {isUseStroke && {e}} + + )} ); } @@ -165,11 +198,15 @@ export default function IMSSTextbox(props: ITextboxProps) { key={currentDialogKey + index} style={{ animationDelay: `${delay}ms`, position: 'relative' }} > - - {e} - {e} - {isUseStroke && {e}} - + {isPlainText ? ( + {e} + ) : ( + + {e} + {e} + {isUseStroke && {e}} + + )} ); }); From 5bff9cf1292119ad3e7082b4ddc72a997289efb8 Mon Sep 17 00:00:00 2001 From: KonshinHaoshin Date: Thu, 12 Mar 2026 22:43:14 +0800 Subject: [PATCH 2/3] fix --- packages/webgal/public/game/scene/start.txt | 1 + .../webgal/src/Stage/TextBox/IMSSTextbox.tsx | 194 ++++++++++++++---- .../src/Stage/TextBox/textbox.module.scss | 9 + 3 files changed, 164 insertions(+), 40 deletions(-) diff --git a/packages/webgal/public/game/scene/start.txt b/packages/webgal/public/game/scene/start.txt index 1e072c7c3..33b41e750 100644 --- a/packages/webgal/public/game/scene/start.txt +++ b/packages/webgal/public/game/scene/start.txt @@ -1,3 +1,4 @@ +:普通文字😀普通文字 setVar:heroine=WebGAL; setVar:egine=WebGAL; choose:简体中文:demo_zh_cn.txt|日本語:demo_ja.txt|English:demo_en.txt|Test:function_test.txt; diff --git a/packages/webgal/src/Stage/TextBox/IMSSTextbox.tsx b/packages/webgal/src/Stage/TextBox/IMSSTextbox.tsx index 86e411eff..67c758847 100644 --- a/packages/webgal/src/Stage/TextBox/IMSSTextbox.tsx +++ b/packages/webgal/src/Stage/TextBox/IMSSTextbox.tsx @@ -9,6 +9,12 @@ import { useSelector } from 'react-redux'; import { RootState } from '@/store/store'; const EMOJI_REGEX = /\p{Extended_Pictographic}|\p{Emoji_Presentation}/u; +const EMOJI_SEGMENT_REGEX = /(\p{Extended_Pictographic}|\p{Emoji_Presentation})(\uFE0F|\uFE0E)?/gu; + +interface TextSegment { + text: string; + isEmoji: boolean; +} function getNodeTextContent(node: ReactNode): string { if (typeof node === 'string' || typeof node === 'number') { @@ -23,9 +29,103 @@ function getNodeTextContent(node: ReactNode): string { return ''; } -function shouldRenderPlainText(node: ReactNode): boolean { - const text = getNodeTextContent(node); - return EMOJI_REGEX.test(text); +function splitTextSegments(text: string): TextSegment[] { + if (!text) return [{ text: '', isEmoji: false }]; + + const segments: TextSegment[] = []; + let lastIndex = 0; + + for (const match of text.matchAll(EMOJI_SEGMENT_REGEX)) { + const matchedText = match[0]; + const matchIndex = match.index ?? 0; + + if (matchIndex > lastIndex) { + segments.push({ + text: text.slice(lastIndex, matchIndex), + isEmoji: false, + }); + } + + segments.push({ + text: matchedText, + isEmoji: true, + }); + lastIndex = matchIndex + matchedText.length; + } + + if (lastIndex < text.length) { + segments.push({ + text: text.slice(lastIndex), + isEmoji: false, + }); + } + + return segments; +} + +function splitNodeSegments(node: ReactNode): TextSegment[] | null { + if (typeof node === 'string' || typeof node === 'number') { + return splitTextSegments(String(node)); + } + + if (isValidElement(node)) { + const text = getNodeTextContent(node); + if (!text || !EMOJI_REGEX.test(text)) { + return null; + } + return splitTextSegments(text); + } + + return null; +} + +function renderStyledText( + text: ReactNode, + styleClassName: string, + styleAllText: string, + isUseStroke: boolean, + applyStyle: ReturnType, + outerClassName: string, + innerClassName: string, +) { + return ( + + {text} + {text} + {isUseStroke && {text}} + + ); +} + +function renderSegments( + segments: TextSegment[], + styleClassName: string, + styleAllText: string, + isUseStroke: boolean, + applyStyle: ReturnType, + outerClassName: 'outer' | 'outerName', + innerClassName: 'inner' | 'innerName', + keyPrefix: string, +) { + return segments.map((segment, segmentIndex) => { + if (!segment.text) return null; + if (segment.isEmoji) { + return ( + + {segment.text} + + ); + } + + return ( + + {renderStyledText(segment.text, styleClassName, styleAllText, isUseStroke, applyStyle, outerClassName, innerClassName)} + + ); + }); } export default function IMSSTextbox(props: ITextboxProps) { @@ -92,31 +192,39 @@ export default function IMSSTextbox(props: ITextboxProps) { } const styleClassName = ' ' + css(style, { label: 'showname' }); const styleAllText = ' ' + css(style_alltext, { label: 'showname' }); - if (shouldRenderPlainText(e)) { - return ( - - {e} - - ); - } + const segments = splitNodeSegments(e); if (isEnhanced) { return ( - - {e} - {e} - {isUseStroke && {e}} - + {segments + ? renderSegments( + segments, + styleClassName, + styleAllText, + isUseStroke, + applyStyle, + 'outerName', + 'innerName', + `showname-${index}`, + ) + : renderStyledText(e, styleClassName, styleAllText, isUseStroke, applyStyle, 'outerName', 'innerName')} ); } return ( - - {e} - {e} - {isUseStroke && {e}} - + {segments + ? renderSegments( + segments, + styleClassName, + styleAllText, + isUseStroke, + applyStyle, + 'outerName', + 'innerName', + `showname-${index}`, + ) + : renderStyledText(e, styleClassName, styleAllText, isUseStroke, applyStyle, 'outerName', 'innerName')} ); }); @@ -167,7 +275,7 @@ export default function IMSSTextbox(props: ITextboxProps) { } const styleClassName = ' ' + css(style); const styleAllText = ' ' + css(style_alltext); - const isPlainText = shouldRenderPlainText(e); + const segments = splitNodeSegments(e); if (allTextIndex < prevLength) { return ( @@ -178,15 +286,18 @@ export default function IMSSTextbox(props: ITextboxProps) { key={currentDialogKey + index} style={{ animationDelay: `${delay}ms`, animationDuration: `${textDuration}ms` }} > - {isPlainText ? ( - {e} - ) : ( - - {e} - {e} - {isUseStroke && {e}} - - )} + {segments + ? renderSegments( + segments, + styleClassName, + styleAllText, + isUseStroke, + applyStyle, + 'outer', + 'inner', + `${currentDialogKey}-${index}-settled`, + ) + : renderStyledText(e, styleClassName, styleAllText, isUseStroke, applyStyle, 'outer', 'inner')} ); } @@ -198,15 +309,18 @@ export default function IMSSTextbox(props: ITextboxProps) { key={currentDialogKey + index} style={{ animationDelay: `${delay}ms`, position: 'relative' }} > - {isPlainText ? ( - {e} - ) : ( - - {e} - {e} - {isUseStroke && {e}} - - )} + {segments + ? renderSegments( + segments, + styleClassName, + styleAllText, + isUseStroke, + applyStyle, + 'outer', + 'inner', + `${currentDialogKey}-${index}-start`, + ) + : renderStyledText(e, styleClassName, styleAllText, isUseStroke, applyStyle, 'outer', 'inner')} ); }); @@ -259,7 +373,7 @@ export default function IMSSTextbox(props: ITextboxProps) { : undefined) } style={{ - fontFamily: font, + fontFamily: `${font}, "Segoe UI Emoji"`, }} >
diff --git a/packages/webgal/src/Stage/TextBox/textbox.module.scss b/packages/webgal/src/Stage/TextBox/textbox.module.scss index c50816bc8..8aa293eb8 100644 --- a/packages/webgal/src/Stage/TextBox/textbox.module.scss +++ b/packages/webgal/src/Stage/TextBox/textbox.module.scss @@ -114,6 +114,15 @@ $height: 330px; position: relative; /* 保持相对定位 */ } +.emojiText { + color: inherit; + white-space: nowrap; + display: inline-block; + vertical-align: baseline; + position: relative; + font-family: inherit, "Segoe UI Emoji", sans-serif; +} + // //.TextBox_textElement_start::before{ // animation: TextDelayShow 700ms ease-out forwards; From 51ebb1fc1c26456974573b166af298afa265f751 Mon Sep 17 00:00:00 2001 From: KonshinHaoshin Date: Thu, 12 Mar 2026 22:52:19 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=20=E6=B7=BB=E5=8A=A0=E4=BA=86=E7=AE=80?= =?UTF-8?q?=E5=8D=95=E7=9A=84=20emoji=20=E6=A3=80=E6=B5=8B=E5=87=BD?= =?UTF-8?q?=E6=95=B0,=E8=B7=B3=E8=BF=87=E7=89=B9=E6=95=88=E6=B8=B2?= =?UTF-8?q?=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/webgal/public/game/scene/start.txt | 1 - .../webgal/src/Stage/TextBox/IMSSTextbox.tsx | 214 +++--------------- .../src/Stage/TextBox/textbox.module.scss | 9 - 3 files changed, 35 insertions(+), 189 deletions(-) diff --git a/packages/webgal/public/game/scene/start.txt b/packages/webgal/public/game/scene/start.txt index 33b41e750..1e072c7c3 100644 --- a/packages/webgal/public/game/scene/start.txt +++ b/packages/webgal/public/game/scene/start.txt @@ -1,4 +1,3 @@ -:普通文字😀普通文字 setVar:heroine=WebGAL; setVar:egine=WebGAL; choose:简体中文:demo_zh_cn.txt|日本語:demo_ja.txt|English:demo_en.txt|Test:function_test.txt; diff --git a/packages/webgal/src/Stage/TextBox/IMSSTextbox.tsx b/packages/webgal/src/Stage/TextBox/IMSSTextbox.tsx index 67c758847..b7648309a 100644 --- a/packages/webgal/src/Stage/TextBox/IMSSTextbox.tsx +++ b/packages/webgal/src/Stage/TextBox/IMSSTextbox.tsx @@ -1,5 +1,5 @@ import styles from './textbox.module.scss'; -import { isValidElement, ReactNode, useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import { WebGAL } from '@/Core/WebGAL'; import { ITextboxProps } from './types'; import useApplyStyle from '@/hooks/useApplyStyle'; @@ -8,125 +8,10 @@ import { textSize } from '@/store/userDataInterface'; import { useSelector } from 'react-redux'; import { RootState } from '@/store/store'; -const EMOJI_REGEX = /\p{Extended_Pictographic}|\p{Emoji_Presentation}/u; -const EMOJI_SEGMENT_REGEX = /(\p{Extended_Pictographic}|\p{Emoji_Presentation})(\uFE0F|\uFE0E)?/gu; - -interface TextSegment { - text: string; - isEmoji: boolean; -} - -function getNodeTextContent(node: ReactNode): string { - if (typeof node === 'string' || typeof node === 'number') { - return String(node); - } - if (Array.isArray(node)) { - return node.map((item) => getNodeTextContent(item)).join(''); - } - if (isValidElement(node)) { - return getNodeTextContent(node.props.children); - } - return ''; -} - -function splitTextSegments(text: string): TextSegment[] { - if (!text) return [{ text: '', isEmoji: false }]; - - const segments: TextSegment[] = []; - let lastIndex = 0; - - for (const match of text.matchAll(EMOJI_SEGMENT_REGEX)) { - const matchedText = match[0]; - const matchIndex = match.index ?? 0; - - if (matchIndex > lastIndex) { - segments.push({ - text: text.slice(lastIndex, matchIndex), - isEmoji: false, - }); - } - - segments.push({ - text: matchedText, - isEmoji: true, - }); - lastIndex = matchIndex + matchedText.length; - } - - if (lastIndex < text.length) { - segments.push({ - text: text.slice(lastIndex), - isEmoji: false, - }); - } - - return segments; -} - -function splitNodeSegments(node: ReactNode): TextSegment[] | null { - if (typeof node === 'string' || typeof node === 'number') { - return splitTextSegments(String(node)); - } - - if (isValidElement(node)) { - const text = getNodeTextContent(node); - if (!text || !EMOJI_REGEX.test(text)) { - return null; - } - return splitTextSegments(text); - } - - return null; -} - -function renderStyledText( - text: ReactNode, - styleClassName: string, - styleAllText: string, - isUseStroke: boolean, - applyStyle: ReturnType, - outerClassName: string, - innerClassName: string, -) { - return ( - - {text} - {text} - {isUseStroke && {text}} - - ); -} - -function renderSegments( - segments: TextSegment[], - styleClassName: string, - styleAllText: string, - isUseStroke: boolean, - applyStyle: ReturnType, - outerClassName: 'outer' | 'outerName', - innerClassName: 'inner' | 'innerName', - keyPrefix: string, -) { - return segments.map((segment, segmentIndex) => { - if (!segment.text) return null; - if (segment.isEmoji) { - return ( - - {segment.text} - - ); - } - - return ( - - {renderStyledText(segment.text, styleClassName, styleAllText, isUseStroke, applyStyle, outerClassName, innerClassName)} - - ); - }); -} +const hasEmoji = (text: any): boolean => { + if (typeof text !== 'string') return false; + return /\p{Emoji}/u.test(text); +}; export default function IMSSTextbox(props: ITextboxProps) { const { @@ -192,39 +77,18 @@ export default function IMSSTextbox(props: ITextboxProps) { } const styleClassName = ' ' + css(style, { label: 'showname' }); const styleAllText = ' ' + css(style_alltext, { label: 'showname' }); - const segments = splitNodeSegments(e); - if (isEnhanced) { - return ( - - {segments - ? renderSegments( - segments, - styleClassName, - styleAllText, - isUseStroke, - applyStyle, - 'outerName', - 'innerName', - `showname-${index}`, - ) - : renderStyledText(e, styleClassName, styleAllText, isUseStroke, applyStyle, 'outerName', 'innerName')} - - ); - } + const skipEffect = hasEmoji(e); return ( - {segments - ? renderSegments( - segments, - styleClassName, - styleAllText, - isUseStroke, - applyStyle, - 'outerName', - 'innerName', - `showname-${index}`, - ) - : renderStyledText(e, styleClassName, styleAllText, isUseStroke, applyStyle, 'outerName', 'innerName')} + {skipEffect ? ( + {e} + ) : ( + + {e} + {e} + {isUseStroke && {e}} + + )} ); }); @@ -275,52 +139,44 @@ export default function IMSSTextbox(props: ITextboxProps) { } const styleClassName = ' ' + css(style); const styleAllText = ' ' + css(style_alltext); - const segments = splitNodeSegments(e); + const skipEffect = hasEmoji(e); if (allTextIndex < prevLength) { return ( - {segments - ? renderSegments( - segments, - styleClassName, - styleAllText, - isUseStroke, - applyStyle, - 'outer', - 'inner', - `${currentDialogKey}-${index}-settled`, - ) - : renderStyledText(e, styleClassName, styleAllText, isUseStroke, applyStyle, 'outer', 'inner')} + {skipEffect ? ( + {e} + ) : ( + + {e} + {e} + {isUseStroke && {e}} + + )} ); } return ( - {segments - ? renderSegments( - segments, - styleClassName, - styleAllText, - isUseStroke, - applyStyle, - 'outer', - 'inner', - `${currentDialogKey}-${index}-start`, - ) - : renderStyledText(e, styleClassName, styleAllText, isUseStroke, applyStyle, 'outer', 'inner')} + {skipEffect ? ( + {e} + ) : ( + + {e} + {e} + {isUseStroke && {e}} + + )} ); }); @@ -373,7 +229,7 @@ export default function IMSSTextbox(props: ITextboxProps) { : undefined) } style={{ - fontFamily: `${font}, "Segoe UI Emoji"`, + fontFamily: font, }} >
diff --git a/packages/webgal/src/Stage/TextBox/textbox.module.scss b/packages/webgal/src/Stage/TextBox/textbox.module.scss index 8aa293eb8..c50816bc8 100644 --- a/packages/webgal/src/Stage/TextBox/textbox.module.scss +++ b/packages/webgal/src/Stage/TextBox/textbox.module.scss @@ -114,15 +114,6 @@ $height: 330px; position: relative; /* 保持相对定位 */ } -.emojiText { - color: inherit; - white-space: nowrap; - display: inline-block; - vertical-align: baseline; - position: relative; - font-family: inherit, "Segoe UI Emoji", sans-serif; -} - // //.TextBox_textElement_start::before{ // animation: TextDelayShow 700ms ease-out forwards;