/**
* The TextMetrics object represents the measurement of a block of text with a specified style.
*
* @class
* @memberOf PIXI
*/
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 || style.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');
// Greedy wrapping algorithm that will wrap words as the line grows longer
// than its horizontal bounds.
let result = '';
const lines = text.split('\n');
const wordWrapWidth = style.wordWrapWidth;
const characterCache = {};
for (let i = 0; i < lines.length; i++)
{
let spaceLeft = wordWrapWidth;
const words = lines[i].split(' ');
for (let j = 0; j < words.length; j++)
{
const wordWidth = context.measureText(words[j]).width;
if (style.breakWords && wordWidth > wordWrapWidth)
{
// Word should be split in the middle
const characters = words[j].split('');
for (let c = 0; c < characters.length; c++)
{
const character = characters[c];
let characterWidth = characterCache[character];
if (characterWidth === undefined)
{
characterWidth = context.measureText(character).width;
characterCache[character] = characterWidth;
}
if (characterWidth > spaceLeft)
{
result += `\n${character}`;
spaceLeft = wordWrapWidth - characterWidth;
}
else
{
if (c === 0)
{
result += ' ';
}
result += character;
spaceLeft -= characterWidth;
}
}
}
else
{
const wordWidthWithSpace = wordWidth + context.measureText(' ').width;
if (j === 0 || wordWidthWithSpace > spaceLeft)
{
// Skip printing the newline if it's the first word of the line that is
// greater than the word wrap width.
if (j > 0)
{
result += '\n';
}
result += words[j];
spaceLeft = wordWrapWidth - wordWidth;
}
else
{
spaceLeft -= wordWidthWithSpace;
result += ` ${words[j]}`;
}
}
}
if (i < lines.length - 1)
{
result += '\n';
}
}
return result;
}
/**
* 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 width = Math.ceil(context.measureText('|MÉq').width);
let baseline = Math.ceil(context.measureText('M').width);
const height = 2 * baseline;
baseline = baseline * 1.4 | 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('|MÉq', 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)
{
if (imagedata[idx + j] !== 255)
{
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;
}
}
/**
* 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 = document.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 = {};