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
JavaScript
;
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');
;
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