mirror of
				https://github.com/unanmed/HumanBreak.git
				synced 2025-10-31 20:32:58 +08:00 
			
		
		
		
	feat: 文本框自动分词断行
This commit is contained in:
		
							parent
							
								
									b679cadd1b
								
							
						
					
					
						commit
						39baab94ae
					
				
							
								
								
									
										1
									
								
								src/core/render/components/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/core/render/components/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| export * from './textbox'; | ||||
							
								
								
									
										191
									
								
								src/core/render/components/textbox.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								src/core/render/components/textbox.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,191 @@ | ||||
| import { MotaOffscreenCanvas2D } from '@/core/fx/canvas2d'; | ||||
| import { defineComponent } from 'vue'; | ||||
| 
 | ||||
| export const enum WordBreak { | ||||
|     /** 不换行 */ | ||||
|     None, | ||||
|     /** 仅空格和连字符等可换行,CJK 字符可任意换行,默认值 */ | ||||
|     Space, | ||||
|     /** 所有字符都可以换行 */ | ||||
|     All | ||||
| } | ||||
| 
 | ||||
| export interface TextContentProps { | ||||
|     text: string; | ||||
|     x?: number; | ||||
|     y?: number; | ||||
|     width?: number; | ||||
|     height?: number; | ||||
|     font?: string; | ||||
|     /** 打字机时间间隔,即两个字出现之间相隔多长时间 */ | ||||
|     interval?: number; | ||||
|     /** 行高 */ | ||||
|     lineHeight?: number; | ||||
|     /** 分词规则 */ | ||||
|     wordBreak?: WordBreak; | ||||
|     /** 行首忽略字符,即不会出现在行首的字符 */ | ||||
|     ignoreLineStart?: Iterable<string>; | ||||
|     /** 行尾忽略字符,即不会出现在行尾的字符 */ | ||||
|     ignoreLineEnd?: Iterable<string>; | ||||
| } | ||||
| 
 | ||||
| interface TextContentData { | ||||
|     text: string; | ||||
|     width: number; | ||||
|     font: string; | ||||
|     /** 分词规则 */ | ||||
|     wordBreak: WordBreak; | ||||
|     /** 行首忽略字符,即不会出现在行首的字符 */ | ||||
|     ignoreLineStart: Set<string>; | ||||
|     /** 行尾忽略字符,即不会出现在行尾的字符 */ | ||||
|     ignoreLineEnd: Set<string>; | ||||
|     /** 会被分词规则识别的文字 */ | ||||
|     breakChars: Set<string>; | ||||
| } | ||||
| 
 | ||||
| export const TextContent = defineComponent((props, ctx) => { | ||||
|     return () => {}; | ||||
| }); | ||||
| 
 | ||||
| export const Textbox = defineComponent((props, ctx) => { | ||||
|     return () => {}; | ||||
| }); | ||||
| 
 | ||||
| let testCanvas: MotaOffscreenCanvas2D; | ||||
| Mota.require('var', 'loading').once('coreInit', () => { | ||||
|     testCanvas = new MotaOffscreenCanvas2D(false); | ||||
|     testCanvas.withGameScale(false); | ||||
|     testCanvas.setHD(false); | ||||
|     testCanvas.size(32, 32); | ||||
|     testCanvas.freeze(); | ||||
| }); | ||||
| 
 | ||||
| /** | ||||
|  * 对文字进行分行操作 | ||||
|  * @param data 文字信息 | ||||
|  */ | ||||
| function splitLines(data: TextContentData) { | ||||
|     const words = breakWords(data); | ||||
|     if (words.length === 1) return [words[0]]; | ||||
| 
 | ||||
|     // 对文字二分,然后计算长度
 | ||||
|     const text = data.text; | ||||
|     let start = 0; | ||||
|     let end = words.length; | ||||
|     let resolved = 0; | ||||
|     let mid = 0; | ||||
| 
 | ||||
|     const res: number[] = []; | ||||
| 
 | ||||
|     const ctx = testCanvas.ctx; | ||||
|     ctx.font = data.font; | ||||
| 
 | ||||
|     console.time(); | ||||
|     while (1) { | ||||
|         const diff = end - start; | ||||
| 
 | ||||
|         if (diff === 1) { | ||||
|             const data1 = ctx.measureText( | ||||
|                 text.slice(words[resolved], words[end]) | ||||
|             ); | ||||
|             if (data1.width <= data.width) { | ||||
|                 res.push(words[end - 1]); | ||||
|             } else { | ||||
|                 res.push(words[start]); | ||||
|             } | ||||
|             if (end === words.length) break; | ||||
|             resolved = start; | ||||
|             end = words.length; | ||||
|         } else { | ||||
|             mid = Math.floor((start + end) / 2); | ||||
|             const chars = text.slice(words[resolved], words[mid]); | ||||
|             const { width } = ctx.measureText(chars); | ||||
|             if (width <= data.width) { | ||||
|                 start = mid; | ||||
|                 if (start === end) end++; | ||||
|             } else { | ||||
|                 end = mid; | ||||
|                 if (start === end) end++; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     console.timeEnd(); | ||||
| 
 | ||||
|     return res; | ||||
| } | ||||
| 
 | ||||
| const defaultsBreak = ' -,.)]}?!;:,。)】?!;:'; | ||||
| const breakSet = new Set(defaultsBreak); | ||||
| 
 | ||||
| /** | ||||
|  * 判断一个文字是否是 CJK 文字 | ||||
|  * @param char 文字的编码 | ||||
|  */ | ||||
| function isCJK(char: number) { | ||||
|     // 参考自 https://blog.csdn.net/brooksychen/article/details/2755395
 | ||||
|     return ( | ||||
|         (char >= 0x4e00 && char <= 0x9fff) || | ||||
|         (char >= 0x3000 && char <= 0x30ff) || | ||||
|         (char >= 0xac00 && char <= 0xd7af) || | ||||
|         (char >= 0xf900 && char <= 0xfaff) || | ||||
|         (char >= 0x3400 && char <= 0x4dbf) || | ||||
|         (char >= 0x20000 && char <= 0x2ebef) || | ||||
|         (char >= 0x30000 && char <= 0x323af) || | ||||
|         (char >= 0x2e80 && char <= 0x2eff) || | ||||
|         (char >= 0x31c0 && char <= 0x31ef) | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * 对文字进行分词操作 | ||||
|  * @param data 文字信息 | ||||
|  */ | ||||
| function breakWords(data: TextContentData) { | ||||
|     let allBreak = false; | ||||
|     const breakChars = breakSet.union(data.breakChars); | ||||
|     switch (data.wordBreak) { | ||||
|         case WordBreak.None: { | ||||
|             return [data.text.length]; | ||||
|         } | ||||
|         case WordBreak.Space: { | ||||
|             allBreak = false; | ||||
|             break; | ||||
|         } | ||||
|         case WordBreak.All: { | ||||
|             allBreak = true; | ||||
|             break; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     console.time(); | ||||
|     const res: number[] = [0]; | ||||
|     const text = data.text; | ||||
|     const { ignoreLineStart, ignoreLineEnd } = data; | ||||
|     for (let pointer = 0; pointer < text.length; pointer++) { | ||||
|         const char = text[pointer]; | ||||
|         const next = text[pointer + 1]; | ||||
| 
 | ||||
|         if (!ignoreLineEnd.has(char) && ignoreLineEnd.has(next)) { | ||||
|             res.push(pointer); | ||||
|             continue; | ||||
|         } | ||||
| 
 | ||||
|         if (ignoreLineStart.has(char) && !ignoreLineStart.has(next)) { | ||||
|             res.push(pointer); | ||||
|             continue; | ||||
|         } | ||||
| 
 | ||||
|         if ( | ||||
|             breakChars.has(char) || | ||||
|             allBreak || | ||||
|             char === '\n' || | ||||
|             isCJK(char.charCodeAt(0)) | ||||
|         ) { | ||||
|             res.push(pointer); | ||||
|             continue; | ||||
|         } | ||||
|     } | ||||
|     res.push(text.length); | ||||
|     console.timeEnd(); | ||||
|     return res; | ||||
| } | ||||
| @ -87,3 +87,4 @@ export * from './shader'; | ||||
| export * from './sprite'; | ||||
| export * from './transform'; | ||||
| export * from './utils'; | ||||
| export * from './components'; | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user