import { _decorator, Director, director, Label, Node, Overflow, RichText, tween, UITransform, Vec3 } from 'cc'; import { StaticTxt } from './StaticTxt'; const { ccclass, property } = _decorator; export class Tools { private static _instance: Tools; ctx: any = null public static get instance(): Tools { if (!this._instance) { this._instance = new Tools(); let canvas = document.createElement('canvas'); this._instance.ctx = canvas.getContext('2d')!; } return this._instance; } } /**RichText元素*/ interface Segment { type: "label" | "image"; value: string; } interface WrapRes { result: string; lastLineWidth: number; } interface LangTxt { labels: Label[]; // Label 组件数组 richTexts: RichText[]; // RichText 组件数组 } export function fixNum(num: number): string { return (num / 10000).toFixed(2) } // 需要代码计算空格处理换行的婆罗米系文字 export function isBrahmic(lang: string) { let arr = ["th", "my"] return arr.indexOf(lang) != -1 } export function getLabelsAndRichTexts(node: Node, langText: LangTxt): LangTxt { const label = node.getComponent(Label); const richText = node.getComponent(RichText); const staticTxt = node.getComponent(StaticTxt) if (label && staticTxt) { langText.labels.push(label); } if (richText && staticTxt) { langText.richTexts.push(richText); } // 遍历所有子节点 node.children.forEach(child => { getLabelsAndRichTexts(child, langText); }); return langText } /**拿到节点下所有需要翻译的, 挂载了StaticTxt脚本的 Label*/ export function getAllLabels(node: Node, result: Label[] = []): Label[] { const label = node.getComponent(Label); const staticTxt = node.getComponent(StaticTxt) if (label && staticTxt) { result.push(label); } // 遍历所有子节点 node.children.forEach(child => { getAllLabels(child, result); }); return result; } /**拿到节点下所有需要翻译的, 挂载了StaticTxt脚本的 Label*/ export function getAllRichTexts(node: Node, result: RichText[] = []): RichText[] { const richText = node.getComponent(RichText); const staticTxt = node.getComponent(StaticTxt) if (richText && staticTxt) { result.push(richText); } // 遍历所有子节点 node.children.forEach(child => { getAllRichTexts(child, result); }); return result; } export function getTranslate(dict, key) { let str = dict[key] ?? key return str; } export function getTimezoneOffsetString(): string { const pad = (n: number) => (n < 10 ? '0' + n : String(n)); const offset = -new Date().getTimezoneOffset(); // 单位是分钟,注意要取反 const sign = offset >= 0 ? "+" : "-"; const hours = pad(Math.floor(Math.abs(offset) / 60)) const minutes = pad(Math.abs(offset) % 60); return `(GMT${sign}${hours}:${minutes})`; } export function updateLang(node: Node, dict, lang:string) { let langText: LangTxt = { labels: [], richTexts: [] } getLabelsAndRichTexts(node, langText) langText.labels.forEach((label, i) => { // console.log(`label.node.name: ${label.node.name}`) const staticTxt = label.node.getComponent(StaticTxt) const bResize = label.overflow == Overflow.RESIZE_HEIGHT let key = staticTxt?.key if (dict[key]) { if (isBrahmic(lang) && bResize) { label.string = wrapTextBySpace(label, dict[key]) } else { label.string = dict[key] } if (label.node.activeInHierarchy) { label.updateRenderData() } } }) langText.richTexts.forEach((richText, i) => { const staticTxt = richText.node.getComponent(StaticTxt) richText.maxWidth = staticTxt.maxWidth richText.string = wrapRichTextBySpace(richText, staticTxt.key, dict,lang) }) return langText } export function wrapRichTextBySpace(richText: RichText, key: string, dict, lang): string { // key like this: Label_Rules_6_14_2 Label_Rules_6_14_3 const segmentArr: Segment[] = []; const regex = /Label_[^<\s]+|]+>/g; let match: RegExpExecArray | null; while ((match = regex.exec(key)) !== null) { if (match[0].startsWith("Label_")) { let key = match[0].split("Label_")[1] if (dict[key]) { segmentArr.push({ type: "label", value: dict[key] }); } } else { segmentArr.push({ type: "image", value: match[0] }); // "" } } // console.log("segmentArr:", segmentArr); /* segmentArr: [ { "type": "label", "value": "Tap" }, { "type": "image", "value": "" }, { "type": "label", "value": "to select the dates of games to be shown in History." } ] */ let wrapResult: WrapRes = { result: "", lastLineWidth: 0 } segmentArr.forEach((part, i) => { if (isBrahmic(lang)) { if (part.type == "label") { wrapResult = wrapTextByRichTextWidth(richText, part.value, wrapResult) } else if (part.type == "image") { let img = part.value.match(/src=['"]([^'"]+)['"]/) const spriteFrame = richText.imageAtlas.getSpriteFrame(img[1]); if (spriteFrame) { const rect = spriteFrame.rect; // 原始图片矩形 // console.log("原始图片宽度:", rect.width); // 图片宽度 + 最后一行宽度 比 最大宽度大,图片换行放到下一行去 if (wrapResult.lastLineWidth + rect.width > richText.maxWidth) { wrapResult.result += "\n" + part.value wrapResult.lastLineWidth = rect.width } else { // 放得下就放在最后一行 wrapResult.result += part.value wrapResult.lastLineWidth += rect.width } } } }else{ // 非婆罗米系文字,引擎自动换行就能处理好 // 不需要手动计算 wrapResult.result += part.value } }) return wrapResult.result } // 富文本换行 export function wrapTextByRichTextWidth(richText: RichText, str: string, lastWrapResult: WrapRes = { result: "", lastLineWidth: 0 }): WrapRes { const fontSize = richText.fontSize; const fontFamily = richText.fontFamily ?? 'Arial' const maxWidth = richText.maxWidth // richText 最大宽度 let wrapResult = lastWrapResult; Tools.instance.ctx.font = `${fontSize}px ${fontFamily}`; const words = str.split(' ') let line = ''; let result = ''; let endIdx = words.length - 1 for (let i = 0; i < words.length; i++) { let word = words[i] // console.log("word:", word) let testLine = line + " " + word const width = wrapResult.lastLineWidth + Tools.instance.ctx.measureText(testLine).width; // console.log("width = ", width, "maxWidth = ", maxWidth) // 长度超了放到下一行去 if (width + 5 > maxWidth && line.length > 0) { if (i == endIdx) { // console.log("最后一个单词长度超了") result += line + '\n' + word; wrapResult.lastLineWidth = Tools.instance.ctx.measureText(word).width; } else { // console.log("单词长度超了,换行") result += line + '\n'; line = word wrapResult.lastLineWidth = 0 } } else { if (i == endIdx) { // console.log("最后一个单词,拼在屁股后面") result += testLine; wrapResult.lastLineWidth = Tools.instance.ctx.measureText(testLine).width; } else { // console.log("拼在屁股后面") line = testLine; } } } // console.log("循环结束, ", result) wrapResult.result += result return wrapResult; } // 按单词换行 export function wrapTextBySpace(label: Label, str: string): string { const fontSize = label.fontSize; const fontFamily = label.fontFamily ?? 'Arial' const maxWidth = label.node.getComponent(UITransform).width; // Label 最大宽度 const words = str.split(' ') let line = ''; let result = ''; // const canvas = document.createElement('canvas'); // const ctx = canvas.getContext('2d')!; Tools.instance.ctx.font = `${fontSize}px ${fontFamily}`; let endIdx = words.length - 1 for (let i = 0; i < words.length; i++) { let word = words[i] // console.log("word = ", word) let testLine = line + " " + word const width = Tools.instance.ctx.measureText(testLine).width; // 长度超了放到下一行去 if (width > maxWidth && line.length > 0) { if (i == endIdx) { result += line + '\n' + word; } else { result += line + '\n'; line = word } } else { if (i == endIdx) { result += testLine; } else { line = testLine; } } } // console.log("循环结束, ", result) return result; } export function waitNextFrame(): Promise { return new Promise(resolve => { director.once(Director.EVENT_BEFORE_UPDATE, () => resolve()); }); } export function hideAllChildren(node: Node) { node.children.forEach(child => { child.active = false }) } export function showAllChildren(node: Node) { node.children.forEach(child => { child.active = true }) } export function showFromBottom(node: Node) { if (!node) { return } let height = node.getComponent(UITransform)?.height if (height) { height = 1920; } node.position = new Vec3(0, -height / 2) tween(node).to(0.15, { position: new Vec3() }).start() } export function hideToBottom(node: Node, callFuc = null) { if (!node) { return } let height = node.getComponent(UITransform)?.height if (height) { height = 1920; } let endPos = new Vec3(0, -height) tween(node).to(0.15, { position: endPos }).call(() => { if (callFuc) { callFuc() } }).start() }