UNPKG

canvas2djs

Version:

HTML5 canvas based game engine

347 lines (313 loc) 11.7 kB
import { ITextLabel, FontStyle, FontWeight } from './sprite/TextLabel'; import { Color } from './Util'; var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); var measuredCache: { [key: string]: MeasuredSize } = {}; var measuredWidthCache: { [key: string]: number } = {}; var cacheCount = 0; function getCacheKey( textFlow: TextFlow[], width: number, fontName: string, fontSize: number, fontWeight: FontWeight, fontStyle: FontStyle, lineHeight: number, wordWrap: boolean, autoResizeWidth: boolean, ) { return [JSON.stringify(textFlow), width, fontName, fontSize, fontStyle, lineHeight, wordWrap, autoResizeWidth].join(':'); } export type TextFlow = { text: string; fontStyle?: FontStyle; fontName?: string; fontWeight?: FontWeight; fontColor?: Color; fontSize?: number; strokeWidth?: number; strokeColor?: Color; } export type TextFragment = TextFlow & { width: number; } export type MeasuredSize = { width: number; height: number; lines: { width: number; fragments: TextFragment[]; }[]; }; export function measureTextWidth(text: string, fontName, fontSize, fontWeight, fontStyle) { let key = [text, fontName, fontSize, fontWeight, fontStyle].join(':'); if (measuredWidthCache[key] != null) { return measuredWidthCache[key]; } ctx.font = fontStyle + ' ' + fontWeight + ' ' + fontSize + 'px ' + fontName; let width = ctx.measureText(text).width; measuredWidthCache[key] = width; return width; } export function measureText( textFlow: TextFlow[], width: number, fontName: string, fontStyle: FontStyle, fontWeight: FontWeight, fontSize: number, lineHeight: number, wordWrap: boolean, autoResizeWidth: boolean, ): MeasuredSize { let cacheKey = getCacheKey(textFlow, width, fontName, fontSize, fontWeight, fontStyle, lineHeight, wordWrap, autoResizeWidth); let cached = measuredCache[cacheKey]; if (cached) { return cached; } let measuredSize: MeasuredSize = { width: width, height: 0, lines: [], }; let remainWidth = width; let lineFragments: TextFragment[] = []; let lineWidth = 0; let textMetrics: TextMetrics; for (let i = 0, flow: TextFlow; flow = textFlow[i]; i++) { let text: string = flow.text; let props: TextFlow = { fontSize, fontStyle, fontName, fontWeight, ...flow, } ctx.font = props.fontStyle + ' ' + props.fontWeight + ' ' + props.fontSize + 'px ' + props.fontName; if (!wordWrap) { textMetrics = ctx.measureText(text); lineFragments.push({ ...props, text: text, width: textMetrics.width, }); lineWidth += textMetrics.width; continue; } let currentPos = 0; let lastMeasuredWidth = 0; let currentLine = ""; let tryLine: string; while (currentPos < text.length) { // console.log("remain width", remainWidth) let breaker = nextBreak(text, currentPos, remainWidth, props.fontSize, autoResizeWidth, width); if (breaker.words) { tryLine = currentLine + breaker.words; textMetrics = ctx.measureText(tryLine); if (autoResizeWidth && !breaker.required) { currentLine = tryLine; lastMeasuredWidth = textMetrics.width; } else if (!autoResizeWidth && textMetrics.width + lineWidth > width) { if (breaker.words.length > 1 && textMetrics.width + lineWidth - props.fontSize <= width) { // console.log(breaker.words, textMetrics.width, lineWidth, props.fontSize, width, remainWidth); tryLine = currentLine + breaker.words.slice(0, breaker.words.length - 1); breaker.words = breaker.words.slice(breaker.words.length - 1); if (breaker.required) { breaker.pos -= 2; } else { breaker.pos -= 1; } lineFragments.push({ ...props, text: tryLine.trim(), width: textMetrics.width - fontSize, }); measuredSize.lines.push({ width: lineWidth + textMetrics.width - fontSize, fragments: lineFragments, }); } else { // console.log(breaker.words, textMetrics.width, lineWidth, props.fontSize, width, remainWidth, currentLine); lineFragments.push({ ...props, text: currentLine.trim(), width: lastMeasuredWidth, }); measuredSize.lines.push({ fragments: lineFragments, width: lineWidth + lastMeasuredWidth }); } measuredSize.height += lineHeight; lineFragments = []; lineWidth = 0; currentLine = breaker.words; lastMeasuredWidth = ctx.measureText(currentLine.trim()).width; remainWidth = width - lastMeasuredWidth; if (breaker.required) { measuredSize.lines.push({ width: lastMeasuredWidth, fragments: [{ ...props, text: currentLine.trim(), width: lastMeasuredWidth, }], }); measuredSize.height += lineHeight; currentLine = ""; lastMeasuredWidth = 0; } } else if (breaker.required) { lineFragments.push({ ...props, text: tryLine.trim(), width: textMetrics.width, }); measuredSize.lines.push({ width: lineWidth + textMetrics.width, fragments: lineFragments, }); measuredSize.height += lineHeight; lineFragments = []; lineWidth = 0; remainWidth = width; lastMeasuredWidth = 0; currentLine = ""; } else { currentLine = tryLine; lastMeasuredWidth = textMetrics.width; remainWidth = width - lastMeasuredWidth; } } else if (breaker.required) { currentLine = currentLine.trim(); if (currentLine.length) { lineFragments.push({ ...props, text: currentLine, width: lastMeasuredWidth, }); } measuredSize.lines.push({ width: lineWidth + lastMeasuredWidth, fragments: lineFragments, }); measuredSize.height += lineHeight; lineFragments = []; lineWidth = 0; currentLine = ""; lastMeasuredWidth = 0; remainWidth = width; } currentPos = breaker.pos; } currentLine = currentLine.trim(); if (currentLine.length) { lineFragments.push({ ...props, text: currentLine, width: lastMeasuredWidth, }); lineWidth += lastMeasuredWidth; remainWidth = width - lineWidth; } } if (lineFragments.length) { measuredSize.lines.push({ width: lineWidth, fragments: lineFragments, }); measuredSize.height += lineHeight; } if (autoResizeWidth) { let max = 0; for (let i = 0, l = measuredSize.lines.length; i < l; i++) { let line = measuredSize.lines[i]; if (line.width > max) { max = line.width; } } measuredSize.width = max; } if (cacheCount > 200) { measuredCache = {}; cacheCount = 0; } measuredSize.width = Math.round(measuredSize.width); measuredSize.height = Math.round(measuredSize.height); measuredCache[cacheKey] = measuredSize; cacheCount += 1; return measuredSize; } type Breaker = { pos: number; words: string; required: boolean; } function nextBreak(text: string, currPos: number, width: number, fontSize: number, autoResizeWidth: boolean, maxWidth: number): Breaker { if (!autoResizeWidth && width < fontSize) { return { pos: currPos, words: "", required: true, }; } let nextWords: string; let required: boolean; let breakPos: number; let pos: number; let num: number; if (autoResizeWidth) { num = text.length - currPos; } else { num = Math.min(text.length - currPos, Math.floor(width / fontSize)); if (num > 0) { var nt = text.slice(currPos); var arr = nt.match(/[\u4e00-\u9fa5]|[\u3002|\uff1f|\uff01|\uff0c|\u3001|\uff1b|\uff1a|\u201c|\u201d|\u2018|\u2019|\uff08|\uff09|\u300a|\u300b|\u3008|\u3009|\u3010|\u3011|\u300e|\u300f|\u300c|\u300d|\ufe43|\ufe44|\u3014|\u3015|\u2026|\u2014|\uff5e|\ufe4f|\uffe5]+|[a-zA-Z0-9]+|\S/g); if (arr) { var i = 0; var n = 0; while (i < arr.length) { var w = arr[i]; var p = nt.indexOf(w); var l = p + w.length; if (l > num && !(i === 0 && maxWidth / fontSize < l)) { break; } n = l; i++; } if (n == 0) { return { pos: currPos, words: "", required: true, } } num = n; } } } // num = autoResizeWidth ? text.length - currPos : Math.min(text.length - currPos, Math.floor(width / fontSize)); nextWords = text.slice(currPos, currPos + num); breakPos = nextWords.indexOf('\n'); required = breakPos > -1; if (required) { nextWords = nextWords.slice(0, breakPos); pos = currPos + breakPos + 1; } else { pos = currPos + num; } return { pos, words: nextWords, required: required }; }