/**
* The TextMetrics object represents the measurement of a block of text with a specified style.
*
* ```js
* let style = new PIXI.TextStyle({fontFamily : 'Arial', fontSize: 24, fill : 0xff1010, align : 'center'})
* let textMetrics = PIXI.TextMetrics.measureText('Your text', style)
* ```
*
* @class
* @memberOf PIXI
*/
//引入小程序补丁
import { documentAlias } from '@ali/pixi-miniprogram-adapter';
export default class TextMetrics
{
/**
* @param {string} text - the text that was measured
* @param {PIXI.TextStyle} style - the style that was measured
* @param {number} width - the measured width of the text
* @param {number} height - the measured height of the text
* @param {array} lines - an array of the lines of text broken by new lines and wrapping if specified in style
* @param {array} lineWidths - an array of the line widths for each line matched to `lines`
* @param {number} lineHeight - the measured line height for this style
* @param {number} maxLineWidth - the maximum line width for all measured lines
* @param {Object} fontProperties - the font properties object from TextMetrics.measureFont
*/
constructor(text, style, width, height, lines, lineWidths, lineHeight, maxLineWidth, fontProperties)
{
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;
}
/**
* Measures the supplied string of text and returns a Rectangle.
*
* @param {string} text - the text to measure.
* @param {PIXI.TextStyle} style - the text style to use for measuring
* @param {boolean} [wordWrap] - optional override for if word-wrap should be applied to the text.
* @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring.
* @return {PIXI.TextMetrics} measured width and height of the text.
*/
static measureText(text, style, wordWrap, canvas = TextMetrics._canvas)
{
wordWrap = (wordWrap === undefined || wordWrap === null) ? style.wordWrap : wordWrap;
const font = style.toFontString();
const fontProperties = TextMetrics.measureFont(font);
const context = canvas.getContext('2d');
context.font = font;
const outputText = wordWrap ? TextMetrics.wordWrap(text, style, canvas) : text;
const lines = outputText.split(/(?:\r\n|\r|\n)/);
const lineWidths = new Array(lines.length);
let maxLineWidth = 0;
for (let i = 0; i < lines.length; i++)
{
const lineWidth = context.measureText(lines[i]).width + ((lines[i].length - 1) * style.letterSpacing);
lineWidths[i] = lineWidth;
maxLineWidth = Math.max(maxLineWidth, lineWidth);
}
let width = maxLineWidth + style.strokeThickness;
if (style.dropShadow)
{
width += style.dropShadowDistance;
}
const lineHeight = style.lineHeight || fontProperties.fontSize + style.strokeThickness;
let height = Math.max(lineHeight, fontProperties.fontSize + style.strokeThickness)
+ ((lines.length - 1) * (lineHeight + style.leading));
if (style.dropShadow)
{
height += style.dropShadowDistance;
}
return new TextMetrics(
text,
style,
width,
height,
lines,
lineWidths,
lineHeight + style.leading,
maxLineWidth,
fontProperties
);
}
/**
* Applies newlines to a string to have it optimally fit into the horizontal
* bounds set by the Text object's wordWrapWidth property.
*
* @private
* @param {string} text - String to apply word wrapping to
* @param {PIXI.TextStyle} style - the style to use when wrapping
* @param {HTMLCanvasElement} [canvas] - optional specification of the canvas to use for measuring.
* @return {string} New string with new lines applied where required
*/
static wordWrap(text, style, canvas = TextMetrics._canvas)
{
const context = canvas.getContext('2d');
let width = 0;
let line = '';
let lines = '';
const cache = {};
const { letterSpacing, whiteSpace } = style;
// How to handle whitespaces
const collapseSpaces = TextMetrics.collapseSpaces(whiteSpace);
const collapseNewlines = TextMetrics.collapseNewlines(whiteSpace);
// whether or not spaces may be added to the beginning of lines
let canPrependSpaces = !collapseSpaces;
// There is letterSpacing after every char except the last one
// t_h_i_s_' '_i_s_' '_a_n_' '_e_x_a_m_p_l_e_' '_!
// so for convenience the above needs to be compared to width + 1 extra letterSpace
// t_h_i_s_' '_i_s_' '_a_n_' '_e_x_a_m_p_l_e_' '_!_
// ________________________________________________
// And then the final space is simply no appended to each line
const wordWrapWidth = style.wordWrapWidth + letterSpacing;
// break text into words, spaces and newline chars
const tokens = TextMetrics.tokenize(text);
for (let i = 0; i < tokens.length; i++)
{
// get the word, space or newlineChar
let token = tokens[i];
// if word is a new line
if (TextMetrics.isNewline(token))
{
// keep the new line
if (!collapseNewlines)
{
lines += TextMetrics.addLine(line);
canPrependSpaces = !collapseSpaces;
line = '';
width = 0;
continue;
}
// if we should collapse new lines
// we simply convert it into a space
token = ' ';
}
// if we should collapse repeated whitespaces
if (collapseSpaces)
{
// check both this and the last tokens for spaces
const currIsBreakingSpace = TextMetrics.isBreakingSpace(token);
const lastIsBreakingSpace = TextMetrics.isBreakingSpace(line[line.length - 1]);
if (currIsBreakingSpace && lastIsBreakingSpace)
{
continue;
}
}
// get word width from cache if possible
const tokenWidth = TextMetrics.getFromCache(token, letterSpacing, cache, context);
// word is longer than desired bounds
if (tokenWidth > wordWrapWidth)
{
// if we are not already at the beginning of a line
if (line !== '')
{
// start newlines for overflow words
lines += TextMetrics.addLine(line);
line = '';
width = 0;
}
// break large word over multiple lines
if (TextMetrics.canBreakWords(token, style.breakWords))
{
// break word into characters
const characters = token.split('');
// loop the characters
for (let j = 0; j < characters.length; j++)
{
let char = characters[j];
let k = 1;
// we are not at the end of the token
while (characters[j + k])
{
const nextChar = characters[j + k];
const lastChar = char[char.length - 1];
// should not split chars
if (!TextMetrics.canBreakChars(lastChar, nextChar, token, j, style.breakWords))
{
// combine chars & move forward one
char += nextChar;
}
else
{
break;
}
k++;
}
j += char.length - 1;
const characterWidth = TextMetrics.getFromCache(char, letterSpacing, cache, context);
if (characterWidth + width > wordWrapWidth)
{
lines += TextMetrics.addLine(line);
canPrependSpaces = false;
line = '';
width = 0;
}
line += char;
width += characterWidth;
}
}
// run word out of the bounds
else
{
// if there are words in this line already
// finish that line and start a new one
if (line.length > 0)
{
lines += TextMetrics.addLine(line);
line = '';
width = 0;
}
const isLastToken = i === tokens.length - 1;
// give it its own line if it's not the end
lines += TextMetrics.addLine(token, !isLastToken);
canPrependSpaces = false;
line = '';
width = 0;
}
}
// word could fit
else
{
// word won't fit because of existing words
// start a new line
if (tokenWidth + width > wordWrapWidth)
{
// if its a space we don't want it
canPrependSpaces = false;
// add a new line
lines += TextMetrics.addLine(line);
// start a new line
line = '';
width = 0;
}
// don't add spaces to the beginning of lines
if (line.length > 0 || !TextMetrics.isBreakingSpace(token) || canPrependSpaces)
{
// add the word to the current line
line += token;
// update width counter
width += tokenWidth;
}
}
}
lines += TextMetrics.addLine(line, false);
return lines;
}
/**
* Convienience function for logging each line added during the wordWrap
* method
*
* @private
* @param {string} line - The line of text to add
* @param {boolean} newLine - Add new line character to end
* @return {string} A formatted line
*/
static addLine(line, newLine = true)
{
line = TextMetrics.trimRight(line);
line = (newLine) ? `${line}\n` : line;
return line;
}
/**
* Gets & sets the widths of calculated characters in a cache object
*
* @private
* @param {string} key The key
* @param {number} letterSpacing The letter spacing
* @param {object} cache The cache
* @param {CanvasRenderingContext2D} context The canvas context
* @return {number} The from cache.
*/
static getFromCache(key, letterSpacing, cache, context)
{
let width = cache[key];
if (width === undefined)
{
const spacing = ((key.length) * letterSpacing);
width = context.measureText(key).width + spacing;
cache[key] = width;
}
return width;
}
/**
* Determines whether we should collapse breaking spaces
*
* @private
* @param {string} whiteSpace The TextStyle property whiteSpace
* @return {boolean} should collapse
*/
static collapseSpaces(whiteSpace)
{
return (whiteSpace === 'normal' || whiteSpace === 'pre-line');
}
/**
* Determines whether we should collapse newLine chars
*
* @private
* @param {string} whiteSpace The white space
* @return {boolean} should collapse
*/
static collapseNewlines(whiteSpace)
{
return (whiteSpace === 'normal');
}
/**
* trims breaking whitespaces from string
*
* @private
* @param {string} text The text
* @return {string} trimmed string
*/
static trimRight(text)
{
if (typeof text !== 'string')
{
return '';
}
for (let i = text.length - 1; i >= 0; i--)
{
const char = text[i];
if (!TextMetrics.isBreakingSpace(char))
{
break;
}
text = text.slice(0, -1);
}
return text;
}
/**
* Determines if char is a newline.
*
* @private
* @param {string} char The character
* @return {boolean} True if newline, False otherwise.
*/
static isNewline(char)
{
if (typeof char !== 'string')
{
return false;
}
return (TextMetrics._newlines.indexOf(char.charCodeAt(0)) >= 0);
}
/**
* Determines if char is a breaking whitespace.
*
* @private
* @param {string} char The character
* @return {boolean} True if whitespace, False otherwise.
*/
static isBreakingSpace(char)
{
if (typeof char !== 'string')
{
return false;
}
return (TextMetrics._breakingSpaces.indexOf(char.charCodeAt(0)) >= 0);
}
/**
* Splits a string into words, breaking-spaces and newLine characters
*
* @private
* @param {string} text The text
* @return {array} A tokenized array
*/
static tokenize(text)
{
const tokens = [];
let token = '';
if (typeof text !== 'string')
{
return tokens;
}
for (let i = 0; i < text.length; i++)
{
const char = text[i];
if (TextMetrics.isBreakingSpace(char) || TextMetrics.isNewline(char))
{
if (token !== '')
{
tokens.push(token);
token = '';
}
tokens.push(char);
continue;
}
token += char;
}
if (token !== '')
{
tokens.push(token);
}
return tokens;
}
/**
* This method exists to be easily overridden
* It allows one to customise which words should break
* Examples are if the token is CJK or numbers.
* It must return a boolean.
*
* @private
* @param {string} token The token
* @param {boolean} breakWords The style attr break words
* @return {boolean} whether to break word or not
*/
static canBreakWords(token, breakWords)
{
return breakWords;
}
/**
* This method exists to be easily overridden
* 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.
*
* @private
* @param {string} char The character
* @param {string} nextChar The next character
* @param {string} token The token/word the characters are from
* @param {number} index The index in the token of the char
* @param {boolean} breakWords The style attr break words
* @return {boolean} whether to break word or not
*/
static canBreakChars(char, nextChar, token, index, breakWords) // eslint-disable-line no-unused-vars
{
return true;
}
/**
* Calculates the ascent, descent and fontSize of a given font-style
*
* @static
* @param {string} font - String representing the style of the font
* @return {PIXI.TextMetrics~FontMetrics} Font properties object
*/
static measureFont(font)
{
// as this method is used for preparing assets, don't recalculate things if we don't need to
if (TextMetrics._fonts[font])
{
return TextMetrics._fonts[font];
}
const properties = {};
const canvas = TextMetrics._canvas;
const context = TextMetrics._context;
context.font = font;
const metricsString = TextMetrics.METRICS_STRING + TextMetrics.BASELINE_SYMBOL;
const width = Math.ceil(context.measureText(metricsString).width);
let baseline = Math.ceil(context.measureText(TextMetrics.BASELINE_SYMBOL).width);
const height = 2 * baseline;
baseline = baseline * TextMetrics.BASELINE_MULTIPLIER | 0;
canvas.width = width;
canvas.height = height;
context.fillStyle = '#f00';
context.fillRect(0, 0, width, height);
context.font = font;
context.textBaseline = 'alphabetic';
context.fillStyle = '#000';
context.fillText(metricsString, 0, baseline);
const imagedata = context.getImageData(0, 0, width, height).data;
const pixels = imagedata.length;
const line = width * 4;
let i = 0;
let idx = 0;
let stop = false;
// ascent. scan from top to bottom until we find a non red pixel
for (i = 0; i < baseline; ++i)
{
for (let j = 0; j < line; j += 4)
{
// !!! 小程序canvas中 getImageData 返回的arraybuffer中 a通道的上限是254 故不能用255判断,而选择254判断
// if (imagedata[idx + j] !== 255)
if (imagedata[idx + j] < 254)
{
stop = true;
break;
}
}
if (!stop)
{
idx += line;
}
else
{
break;
}
}
properties.ascent = baseline - i;
idx = pixels - line;
stop = false;
// descent. scan from bottom to top until we find a non red pixel
for (i = height; i > baseline; --i)
{
for (let j = 0; j < line; j += 4)
{
if (imagedata[idx + j] !== 255)
{
stop = true;
break;
}
}
if (!stop)
{
idx -= line;
}
else
{
break;
}
}
properties.descent = i - baseline;
properties.fontSize = properties.ascent + properties.descent;
TextMetrics._fonts[font] = properties;
return properties;
}
/**
* Clear font metrics in metrics cache.
*
* @static
* @param {string} [font] - font name. If font name not set then clear cache for all fonts.
*/
static clearMetrics(font = '')
{
if (font)
{
delete TextMetrics._fonts[font];
}
else
{
TextMetrics._fonts = {};
}
}
}
/**
* Internal return object for {@link PIXI.TextMetrics.measureFont `TextMetrics.measureFont`}.
* @class FontMetrics
* @memberof PIXI.TextMetrics~
* @property {number} ascent - The ascent distance
* @property {number} descent - The descent distance
* @property {number} fontSize - Font size from ascent to descent
*/
const canvas = documentAlias.createElement('canvas');
canvas.width = canvas.height = 10;
/**
* Cached canvas element for measuring text
* @memberof PIXI.TextMetrics
* @type {HTMLCanvasElement}
* @private
*/
TextMetrics._canvas = canvas;
/**
* Cache for context to use.
* @memberof PIXI.TextMetrics
* @type {CanvasRenderingContext2D}
* @private
*/
TextMetrics._context = canvas.getContext('2d');
/**
* Cache of PIXI.TextMetrics~FontMetrics objects.
* @memberof PIXI.TextMetrics
* @type {Object}
* @private
*/
TextMetrics._fonts = {};
/**
* String used for calculate font metrics.
* @static
* @memberof PIXI.TextMetrics
* @name METRICS_STRING
* @type {string}
* @default |Éq
*/
TextMetrics.METRICS_STRING = '|Éq';
/**
* Baseline symbol for calculate font metrics.
* @static
* @memberof PIXI.TextMetrics
* @name BASELINE_SYMBOL
* @type {string}
* @default M
*/
TextMetrics.BASELINE_SYMBOL = 'M';
/**
* Baseline multiplier for calculate font metrics.
* @static
* @memberof PIXI.TextMetrics
* @name BASELINE_MULTIPLIER
* @type {number}
* @default 1.4
*/
TextMetrics.BASELINE_MULTIPLIER = 1.4;
/**
* Cache of new line chars.
* @memberof PIXI.TextMetrics
* @type {number[]}
* @private
*/
TextMetrics._newlines = [
0x000A, // line feed
0x000D, // carriage return
];
/**
* Cache of breaking spaces.
* @memberof PIXI.TextMetrics
* @type {number[]}
* @private
*/
TextMetrics._breakingSpaces = [
0x0009, // character tabulation
0x0020, // space
0x2000, // en quad
0x2001, // em quad
0x2002, // en space
0x2003, // em space
0x2004, // three-per-em space
0x2005, // four-per-em space
0x2006, // six-per-em space
0x2008, // punctuation space
0x2009, // thin space
0x200A, // hair space
0x205F, // medium mathematical space
0x3000, // ideographic space
];