UNPKG

pixi.js

Version:

<p align="center"> <a href="https://pixijs.com" target="_blank" rel="noopener noreferrer"> <img height="150" src="https://files.pixijs.download/branding/pixijs-logo-transparent-dark.svg?v=1" alt="PixiJS logo"> </a> </p> <br/> <p align="center">

422 lines (418 loc) 16.9 kB
'use strict'; var tinyLru = require('tiny-lru'); var adapter = require('../../../environment/adapter.js'); var measureTaggedText = require('./utils/measureTaggedText.js'); var parseTaggedText = require('./utils/parseTaggedText.js'); var textTokenization = require('./utils/textTokenization.js'); var wordWrap = require('./utils/wordWrap.js'); "use strict"; const contextSettings = { // TextMetrics requires getImageData readback for measuring fonts. willReadFrequently: true }; const _CanvasTextMetrics = class _CanvasTextMetrics { /** * Checking that we can use modern canvas 2D API. * * Note: This is an unstable API, Chrome < 94 use `textLetterSpacing`, later versions use `letterSpacing`. * @see CanvasTextMetrics.experimentalLetterSpacing * @see https://developer.mozilla.org/en-US/docs/Web/API/ICanvasRenderingContext2D/letterSpacing * @see https://developer.chrome.com/origintrials/#/view_trial/3585991203293757441 */ static get experimentalLetterSpacingSupported() { let result = _CanvasTextMetrics._experimentalLetterSpacingSupported; if (result === void 0) { const proto = adapter.DOMAdapter.get().getCanvasRenderingContext2D().prototype; result = _CanvasTextMetrics._experimentalLetterSpacingSupported = "letterSpacing" in proto || "textLetterSpacing" in proto; } return result; } /** * @param text - the text that was measured * @param style - the style that was measured * @param width - the measured width of the text * @param height - the measured height of the text * @param lines - an array of the lines of text broken by new lines and wrapping if specified in style * @param lineWidths - an array of the line widths for each line matched to `lines` * @param lineHeight - the measured line height for this style * @param maxLineWidth - the maximum line width for all measured lines * @param fontProperties - the font properties object from TextMetrics.measureFont * @param taggedData - optional object containing tagged text specific data * @param taggedData.runsByLine - per-line style runs for tagged text * @param taggedData.lineAscents - per-line ascent values for tagged text * @param taggedData.lineDescents - per-line descent values for tagged text * @param taggedData.lineHeights - per-line height values for tagged text * @param taggedData.hasDropShadow - whether any run has a drop shadow */ constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties, taggedData) { this.text = text; this.style = style; this.width = width; this.height = height; this.lines = lines; this.lineWidths = lineWidths; this.lineHeight = lineHeight; this.maxLineWidth = maxLineWidth; this.fontProperties = fontProperties; if (taggedData) { this.runsByLine = taggedData.runsByLine; this.lineAscents = taggedData.lineAscents; this.lineDescents = taggedData.lineDescents; this.lineHeights = taggedData.lineHeights; this.hasDropShadow = taggedData.hasDropShadow; } } /** * Measures the supplied string of text and returns a Rectangle. * @param text - The text to measure. * @param style - The text style to use for measuring * @param canvas - optional specification of the canvas to use for measuring. * @param wordWrap * @returns Measured width and height of the text. */ static measureText(text = " ", style, canvas = _CanvasTextMetrics._canvas, wordWrap2 = style.wordWrap) { const textKey = `${text}-${style.styleKey}-wordWrap-${wordWrap2}`; if (_CanvasTextMetrics._measurementCache.has(textKey)) { return _CanvasTextMetrics._measurementCache.get(textKey); } const isTagged = parseTaggedText.hasTagStyles(style) && parseTaggedText.hasTagMarkup(text); if (isTagged) { const result = measureTaggedText.measureTaggedText( text, style, wordWrap2, _CanvasTextMetrics._context, _CanvasTextMetrics._measureText, _CanvasTextMetrics.measureFont, _CanvasTextMetrics.canBreakChars, _CanvasTextMetrics.wordWrapSplit ); const measurements2 = new _CanvasTextMetrics( text, style, result.width, result.height, result.lines, result.lineWidths, result.lineHeight, result.maxLineWidth, result.fontProperties, { runsByLine: result.runsByLine, lineAscents: result.lineAscents, lineDescents: result.lineDescents, lineHeights: result.lineHeights, hasDropShadow: result.hasDropShadow } ); _CanvasTextMetrics._measurementCache.set(textKey, measurements2); return measurements2; } const font = style._fontString; const fontProperties = _CanvasTextMetrics.measureFont(font); if (fontProperties.fontSize === 0) { fontProperties.fontSize = style.fontSize; fontProperties.ascent = style.fontSize; fontProperties.descent = 0; } const context = _CanvasTextMetrics._context; context.font = font; const outputText = wordWrap2 ? _CanvasTextMetrics._wordWrap(text, style, canvas) : text; const lines = outputText.split(textTokenization.NEWLINE_MATCH_REGEX); const lineWidths = new Array(lines.length); let maxLineWidth = 0; for (let i = 0; i < lines.length; i++) { const lineWidth = _CanvasTextMetrics._measureText(lines[i], style.letterSpacing, context); lineWidths[i] = lineWidth; maxLineWidth = Math.max(maxLineWidth, lineWidth); } const strokeWidth = style._stroke?.width ?? 0; const lineHeight = style.lineHeight || fontProperties.fontSize; const baseWidth = _CanvasTextMetrics._getAlignWidth(maxLineWidth, style, wordWrap2); const width = _CanvasTextMetrics._adjustWidthForStyle(baseWidth, style); const baseHeight = Math.max(lineHeight, fontProperties.fontSize + strokeWidth) + (lines.length - 1) * (lineHeight + style.leading); const height = _CanvasTextMetrics._adjustHeightForStyle(baseHeight, style); const measurements = new _CanvasTextMetrics( text, style, width, height, lines, lineWidths, lineHeight + style.leading, maxLineWidth, fontProperties ); _CanvasTextMetrics._measurementCache.set(textKey, measurements); return measurements; } /** * Adjusts the measured width to account for stroke and drop shadow. * @param baseWidth - The base content width * @param style - The text style * @returns The adjusted width */ static _adjustWidthForStyle(baseWidth, style) { const strokeWidth = style._stroke?.width || 0; let width = baseWidth + strokeWidth; if (style.dropShadow) { width += style.dropShadow.distance; } return width; } /** * Adjusts the measured height to account for drop shadow. * @param baseHeight - The base content height * @param style - The text style * @returns The adjusted height */ static _adjustHeightForStyle(baseHeight, style) { let height = baseHeight; if (style.dropShadow) { height += style.dropShadow.distance; } return height; } /** * Calculates the base width for alignment purposes. * When word wrap is enabled with center/right alignment, uses wordWrapWidth. * @param maxLineWidth - The maximum line width * @param style - The text style * @param wordWrapEnabled - Whether word wrap is enabled * @returns The width to use for alignment calculations */ static _getAlignWidth(maxLineWidth, style, wordWrapEnabled) { const useWrapWidth = wordWrapEnabled && style.align !== "left"; return useWrapWidth ? Math.max(maxLineWidth, style.wordWrapWidth) : maxLineWidth; } /** * Measures the rendered width of a string, accounting for letter spacing and using the provided context. * @param text - The text to measure * @param letterSpacing - Letter spacing in pixels * @param context - Canvas 2D context * @returns The measured width of the text with spacing * @internal */ static _measureText(text, letterSpacing, context) { let useExperimentalLetterSpacing = false; if (_CanvasTextMetrics.experimentalLetterSpacingSupported) { if (_CanvasTextMetrics.experimentalLetterSpacing) { context.letterSpacing = `${letterSpacing}px`; context.textLetterSpacing = `${letterSpacing}px`; useExperimentalLetterSpacing = true; } else { context.letterSpacing = "0px"; context.textLetterSpacing = "0px"; } } const metrics = context.measureText(text); let metricWidth = metrics.width; const actualBoundingBoxLeft = -(metrics.actualBoundingBoxLeft ?? 0); const actualBoundingBoxRight = metrics.actualBoundingBoxRight ?? 0; let boundsWidth = actualBoundingBoxRight - actualBoundingBoxLeft; if (metricWidth > 0) { if (useExperimentalLetterSpacing) { metricWidth -= letterSpacing; boundsWidth -= letterSpacing; } else { const val = (_CanvasTextMetrics.graphemeSegmenter(text).length - 1) * letterSpacing; metricWidth += val; boundsWidth += val; } } return Math.max(metricWidth, boundsWidth); } /** * Applies newlines to a string to have it optimally fit into the horizontal * bounds set by the Text object's wordWrapWidth property. * @param text - String to apply word wrapping to * @param style - the style to use when wrapping * @param canvas - optional specification of the canvas to use for measuring. * @returns New string with new lines applied where required */ static _wordWrap(text, style, canvas = _CanvasTextMetrics._canvas) { return wordWrap.wordWrap( text, style, canvas, _CanvasTextMetrics._measureText, _CanvasTextMetrics.canBreakWords, _CanvasTextMetrics.canBreakChars, _CanvasTextMetrics.wordWrapSplit ); } /** * Determines if char is a breaking whitespace. * * It allows one to determine whether char should be a breaking whitespace * For example certain characters in CJK langs or numbers. * It must return a boolean. * @param char - The character * @param [_nextChar] - The next character * @returns True if whitespace, False otherwise. */ static isBreakingSpace(char, _nextChar) { return textTokenization.isBreakingSpace(char, _nextChar); } /** * Overridable helper method used internally by TextMetrics, exposed to allow customizing the class's behavior. * * It allows one to customise which words should break * Examples are if the token is CJK or numbers. * It must return a boolean. * @param _token - The token * @param breakWords - The style attr break words * @returns Whether to break word or not */ static canBreakWords(_token, breakWords) { return breakWords; } /** * Overridable helper method used internally by TextMetrics, exposed to allow customizing the class's behavior. * * It allows one to determine whether a pair of characters * should be broken by newlines * For example certain characters in CJK langs or numbers. * It must return a boolean. * @param _char - The character * @param _nextChar - The next character * @param _token - The token/word the characters are from * @param _index - The index in the token of the char * @param _breakWords - The style attr break words * @returns whether to break word or not */ static canBreakChars(_char, _nextChar, _token, _index, _breakWords) { return true; } /** * Overridable helper method used internally by TextMetrics, exposed to allow customizing the class's behavior. * * It is called when a token (usually a word) has to be split into separate pieces * in order to determine the point to break a word. * It must return an array of characters. * @param token - The token to split * @returns The characters of the token * @see CanvasTextMetrics.graphemeSegmenter */ static wordWrapSplit(token) { return _CanvasTextMetrics.graphemeSegmenter(token); } /** * Calculates the ascent, descent and fontSize of a given font-style * @param font - String representing the style of the font * @returns Font properties object */ static measureFont(font) { if (_CanvasTextMetrics._fonts[font]) { return _CanvasTextMetrics._fonts[font]; } const context = _CanvasTextMetrics._context; context.font = font; const metrics = context.measureText(_CanvasTextMetrics.METRICS_STRING + _CanvasTextMetrics.BASELINE_SYMBOL); const ascent = metrics.actualBoundingBoxAscent ?? 0; const descent = metrics.actualBoundingBoxDescent ?? 0; const properties = { ascent, descent, fontSize: ascent + descent }; _CanvasTextMetrics._fonts[font] = properties; return properties; } /** * Clear font metrics in metrics cache. * @param {string} [font] - font name. If font name not set then clear cache for all fonts. */ static clearMetrics(font = "") { if (font) { delete _CanvasTextMetrics._fonts[font]; } else { _CanvasTextMetrics._fonts = {}; } } /** * Cached canvas element for measuring text * TODO: this should be private, but isn't because of backward compat, will fix later. * @ignore */ static get _canvas() { if (!_CanvasTextMetrics.__canvas) { let canvas; try { const c = new OffscreenCanvas(0, 0); const context = c.getContext("2d", contextSettings); if (context?.measureText) { _CanvasTextMetrics.__canvas = c; return c; } canvas = adapter.DOMAdapter.get().createCanvas(); } catch (_cx) { canvas = adapter.DOMAdapter.get().createCanvas(); } canvas.width = canvas.height = 10; _CanvasTextMetrics.__canvas = canvas; } return _CanvasTextMetrics.__canvas; } /** * TODO: this should be private, but isn't because of backward compat, will fix later. * @ignore */ static get _context() { if (!_CanvasTextMetrics.__context) { _CanvasTextMetrics.__context = _CanvasTextMetrics._canvas.getContext("2d", contextSettings); } return _CanvasTextMetrics.__context; } }; /** * String used for calculate font metrics. * These characters are all tall to help calculate the height required for text. */ _CanvasTextMetrics.METRICS_STRING = "|\xC9q\xC5"; /** Baseline symbol for calculate font metrics. */ _CanvasTextMetrics.BASELINE_SYMBOL = "M"; /** Baseline multiplier for calculate font metrics. */ _CanvasTextMetrics.BASELINE_MULTIPLIER = 1.4; /** Height multiplier for setting height of canvas to calculate font metrics. */ _CanvasTextMetrics.HEIGHT_MULTIPLIER = 2; /** * A Unicode "character", or "grapheme cluster", can be composed of multiple Unicode code points, * such as letters with diacritical marks (e.g. `'\u0065\u0301'`, letter e with acute) * or emojis with modifiers (e.g. `'\uD83E\uDDD1\u200D\uD83D\uDCBB'`, technologist). * The new `Intl.Segmenter` API in ES2022 can split the string into grapheme clusters correctly. If it is not available, * PixiJS will fallback to use the iterator of String, which can only spilt the string into code points. * If you want to get full functionality in environments that don't support `Intl.Segmenter` (such as Firefox), * you can use other libraries such as [grapheme-splitter]{@link https://www.npmjs.com/package/grapheme-splitter} * or [graphemer]{@link https://www.npmjs.com/package/graphemer} to create a polyfill. Since these libraries can be * relatively large in size to handle various Unicode grapheme clusters properly, PixiJS won't use them directly. */ _CanvasTextMetrics.graphemeSegmenter = (() => { if (typeof Intl?.Segmenter === "function") { const segmenter = new Intl.Segmenter(); return (s) => { const segments = segmenter.segment(s); const result = []; let i = 0; for (const segment of segments) { result[i++] = segment.segment; } return result; }; } return (s) => [...s]; })(); /** * New rendering behavior for letter-spacing which uses Chrome's new native API. This will * lead to more accurate letter-spacing results because it does not try to manually draw * each character. However, this Chrome API is experimental and may not serve all cases yet. * @see CanvasTextMetrics.experimentalLetterSpacingSupported */ _CanvasTextMetrics.experimentalLetterSpacing = false; /** Cache of {@link TextMetrics.FontMetrics} objects. */ _CanvasTextMetrics._fonts = {}; /** Cache for measured text metrics */ _CanvasTextMetrics._measurementCache = tinyLru.lru(1e3); let CanvasTextMetrics = _CanvasTextMetrics; exports.CanvasTextMetrics = CanvasTextMetrics; //# sourceMappingURL=CanvasTextMetrics.js.map