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">
494 lines (490 loc) • 19.8 kB
JavaScript
'use strict';
var Color = require('../../../color/Color.js');
var Rectangle = require('../../../maths/shapes/Rectangle.js');
var CanvasPool = require('../../../rendering/renderers/shared/texture/CanvasPool.js');
var getCanvasBoundingBox = require('../../../utils/canvas/getCanvasBoundingBox.js');
var CanvasTextMetrics = require('./CanvasTextMetrics.js');
var fontStringFromTextStyle = require('./utils/fontStringFromTextStyle.js');
var getCanvasFillStyle = require('./utils/getCanvasFillStyle.js');
"use strict";
const tempRect = new Rectangle.Rectangle();
function countSpaces(text) {
let count = 0;
for (let i = 0; i < text.length; i++) {
if (text.charCodeAt(i) === 32) count++;
}
return count;
}
class CanvasTextGeneratorClass {
/**
* Creates a canvas with the specified text rendered to it.
*
* Generates a canvas of appropriate size, renders the text with the provided style,
* and returns both the canvas/context and a Rectangle representing the text bounds.
*
* When trim is enabled in the style, the frame will represent the bounds of the
* non-transparent pixels, which can be smaller than the full canvas.
* @param options - The options for generating the text canvas
* @param options.text - The text to render
* @param options.style - The style to apply to the text
* @param options.resolution - The resolution of the canvas (defaults to 1)
* @param options.padding
* @returns An object containing the canvas/context and the frame (bounds) of the text
*/
getCanvasAndContext(options) {
const { text, style, resolution = 1 } = options;
const padding = style._getFinalPadding();
const measured = CanvasTextMetrics.CanvasTextMetrics.measureText(text || " ", style);
const width = Math.ceil(Math.ceil(Math.max(1, measured.width) + padding * 2) * resolution);
const height = Math.ceil(Math.ceil(Math.max(1, measured.height) + padding * 2) * resolution);
const canvasAndContext = CanvasPool.CanvasPool.getOptimalCanvasAndContext(width, height);
this._renderTextToCanvas(style, padding, resolution, canvasAndContext, measured);
const frame = style.trim ? getCanvasBoundingBox.getCanvasBoundingBox({ canvas: canvasAndContext.canvas, width, height, resolution: 1, output: tempRect }) : tempRect.set(0, 0, width, height);
return {
canvasAndContext,
frame
};
}
/**
* Returns a canvas and context to the pool.
*
* This should be called when you're done with the canvas to allow reuse
* and prevent memory leaks.
* @param canvasAndContext - The canvas and context to return to the pool
*/
returnCanvasAndContext(canvasAndContext) {
CanvasPool.CanvasPool.returnCanvasAndContext(canvasAndContext);
}
/**
* Renders text to its canvas, and updates its texture.
* @param style - The style of the text
* @param padding - The padding of the text
* @param resolution - The resolution of the text
* @param canvasAndContext - The canvas and context to render the text to
* @param measured - Pre-measured text metrics to avoid duplicate measurement
*/
_renderTextToCanvas(style, padding, resolution, canvasAndContext, measured) {
if (measured.runsByLine && measured.runsByLine.length > 0) {
this._renderTaggedTextToCanvas(measured, style, padding, resolution, canvasAndContext);
return;
}
const { canvas, context } = canvasAndContext;
const font = fontStringFromTextStyle.fontStringFromTextStyle(style);
const lines = measured.lines;
const lineHeight = measured.lineHeight;
const lineWidths = measured.lineWidths;
const maxLineWidth = measured.maxLineWidth;
const fontProperties = measured.fontProperties;
const height = canvas.height;
context.resetTransform();
context.scale(resolution, resolution);
context.textBaseline = style.textBaseline;
if (style._stroke?.width) {
const strokeStyle = style._stroke;
context.lineWidth = strokeStyle.width;
context.miterLimit = strokeStyle.miterLimit;
context.lineJoin = strokeStyle.join;
context.lineCap = strokeStyle.cap;
}
context.font = font;
let linePositionX;
let linePositionY;
const passesCount = style.dropShadow ? 2 : 1;
const alignWidth = style.wordWrap ? Math.max(style.wordWrapWidth, maxLineWidth) : maxLineWidth;
const strokeWidth = style._stroke?.width ?? 0;
const halfStroke = strokeWidth / 2;
let linePositionYShift = (lineHeight - fontProperties.fontSize) / 2;
if (lineHeight - fontProperties.fontSize < 0) {
linePositionYShift = 0;
}
for (let i = 0; i < passesCount; ++i) {
const isShadowPass = style.dropShadow && i === 0;
const dsOffsetText = isShadowPass ? Math.ceil(Math.max(1, height) + padding * 2) : 0;
const dsOffsetShadow = dsOffsetText * resolution;
if (isShadowPass) {
this._setupDropShadow(context, style, resolution, dsOffsetShadow);
} else {
const gradientBounds = style._gradientBounds;
const gradientOffset = style._gradientOffset;
if (gradientBounds) {
const gradientMetrics = {
width: gradientBounds.width,
height: gradientBounds.height,
lineHeight: gradientBounds.height,
lines: measured.lines
};
this._setFillAndStrokeStyles(
context,
style,
gradientMetrics,
padding,
halfStroke,
gradientOffset?.x ?? 0,
gradientOffset?.y ?? 0
);
} else if (gradientOffset) {
this._setFillAndStrokeStyles(
context,
style,
measured,
padding,
halfStroke,
gradientOffset.x,
gradientOffset.y
);
} else {
this._setFillAndStrokeStyles(context, style, measured, padding, halfStroke);
}
context.shadowColor = "rgba(0,0,0,0)";
}
for (let j = 0; j < lines.length; j++) {
linePositionX = halfStroke;
linePositionY = halfStroke + j * lineHeight + fontProperties.ascent + linePositionYShift;
linePositionX += this._getAlignmentOffset(lineWidths[j], alignWidth, style.align);
let wordSpacing = 0;
if (style.align === "justify" && style.wordWrap && j < lines.length - 1) {
const spaces = countSpaces(lines[j]);
if (spaces > 0) {
wordSpacing = (alignWidth - lineWidths[j]) / spaces;
}
}
if (style._stroke?.width) {
this._drawLetterSpacing(
lines[j],
style,
canvasAndContext,
linePositionX + padding,
linePositionY + padding - dsOffsetText,
true,
wordSpacing
);
}
if (style._fill !== void 0) {
this._drawLetterSpacing(
lines[j],
style,
canvasAndContext,
linePositionX + padding,
linePositionY + padding - dsOffsetText,
false,
wordSpacing
);
}
}
}
}
/**
* Renders tagged text (with per-run styles) to canvas.
* @param measured - The measured text metrics containing runsByLine
* @param style - The base text style
* @param padding - The padding of the text
* @param resolution - The resolution of the text
* @param canvasAndContext - The canvas and context to render to
*/
_renderTaggedTextToCanvas(measured, style, padding, resolution, canvasAndContext) {
const { canvas, context } = canvasAndContext;
const { runsByLine, lineWidths, maxLineWidth, lineAscents, lineHeights, hasDropShadow } = measured;
const height = canvas.height;
context.resetTransform();
context.scale(resolution, resolution);
context.textBaseline = style.textBaseline;
const passesCount = hasDropShadow ? 2 : 1;
const alignWidth = style.wordWrap ? Math.max(style.wordWrapWidth, maxLineWidth) : maxLineWidth;
let maxStrokeWidth = style._stroke?.width ?? 0;
for (const lineRuns of runsByLine) {
for (const run of lineRuns) {
const w = run.style._stroke?.width ?? 0;
if (w > maxStrokeWidth) maxStrokeWidth = w;
}
}
const halfStroke = maxStrokeWidth / 2;
const runDataByLine = [];
for (let lineIndex = 0; lineIndex < runsByLine.length; lineIndex++) {
const lineRuns = runsByLine[lineIndex];
const runData = [];
for (const run of lineRuns) {
const font = fontStringFromTextStyle.fontStringFromTextStyle(run.style);
context.font = font;
runData.push({
width: CanvasTextMetrics.CanvasTextMetrics._measureText(run.text, run.style.letterSpacing, context),
font
});
}
runDataByLine.push(runData);
}
for (let pass = 0; pass < passesCount; ++pass) {
const isShadowPass = hasDropShadow && pass === 0;
const dsOffsetText = isShadowPass ? Math.ceil(Math.max(1, height) + padding * 2) : 0;
const dsOffsetShadow = dsOffsetText * resolution;
if (!isShadowPass) {
context.shadowColor = "rgba(0,0,0,0)";
}
let currentY = halfStroke;
for (let lineIndex = 0; lineIndex < runsByLine.length; lineIndex++) {
const lineRuns = runsByLine[lineIndex];
const lineWidth = lineWidths[lineIndex];
const lineAscent = lineAscents[lineIndex];
const currentLineHeight = lineHeights[lineIndex];
const lineRunData = runDataByLine[lineIndex];
let linePositionX = halfStroke;
linePositionX += this._getAlignmentOffset(lineWidth, alignWidth, style.align);
let wordSpacing = 0;
if (style.align === "justify" && style.wordWrap && lineIndex < runsByLine.length - 1) {
let totalSpaces = 0;
for (const run of lineRuns) {
totalSpaces += countSpaces(run.text);
}
if (totalSpaces > 0) {
wordSpacing = (alignWidth - lineWidth) / totalSpaces;
}
}
const linePositionY = currentY + lineAscent;
let runX = linePositionX + padding;
for (let runIndex = 0; runIndex < lineRuns.length; runIndex++) {
const run = lineRuns[runIndex];
const { width: runWidth, font: runFont } = lineRunData[runIndex];
context.font = runFont;
context.textBaseline = run.style.textBaseline;
if (run.style._stroke?.width) {
const runStroke = run.style._stroke;
context.lineWidth = runStroke.width;
context.miterLimit = runStroke.miterLimit;
context.lineJoin = runStroke.join;
context.lineCap = runStroke.cap;
if (isShadowPass) {
if (run.style.dropShadow) {
this._setupDropShadow(
context,
run.style,
resolution,
dsOffsetShadow
);
} else {
const spacesSkipped = countSpaces(run.text);
runX += runWidth + spacesSkipped * wordSpacing;
continue;
}
} else {
const runFontProps = CanvasTextMetrics.CanvasTextMetrics.measureFont(runFont);
const runHeight = run.style.lineHeight || runFontProps.fontSize;
const runMetrics = {
width: runWidth,
height: runHeight,
lineHeight: runHeight,
lines: [run.text]
};
context.strokeStyle = getCanvasFillStyle.getCanvasFillStyle(
runStroke,
context,
runMetrics,
padding * 2,
runX - padding,
currentY
);
}
this._drawLetterSpacing(
run.text,
run.style,
canvasAndContext,
runX,
linePositionY + padding - dsOffsetText,
true,
wordSpacing
);
}
const spacesInRun = countSpaces(run.text);
runX += runWidth + spacesInRun * wordSpacing;
}
runX = linePositionX + padding;
for (let runIndex = 0; runIndex < lineRuns.length; runIndex++) {
const run = lineRuns[runIndex];
const { width: runWidth, font: runFont } = lineRunData[runIndex];
context.font = runFont;
context.textBaseline = run.style.textBaseline;
if (run.style._fill !== void 0) {
if (isShadowPass) {
if (run.style.dropShadow) {
this._setupDropShadow(
context,
run.style,
resolution,
dsOffsetShadow
);
} else {
const spacesSkipped = countSpaces(run.text);
runX += runWidth + spacesSkipped * wordSpacing;
continue;
}
} else {
const runFontProps = CanvasTextMetrics.CanvasTextMetrics.measureFont(runFont);
const runHeight = run.style.lineHeight || runFontProps.fontSize;
const runMetrics = {
width: runWidth,
height: runHeight,
lineHeight: runHeight,
lines: [run.text]
};
context.fillStyle = getCanvasFillStyle.getCanvasFillStyle(
run.style._fill,
context,
runMetrics,
padding * 2,
runX - padding,
currentY
);
}
this._drawLetterSpacing(
run.text,
run.style,
canvasAndContext,
runX,
linePositionY + padding - dsOffsetText,
false,
wordSpacing
);
}
const spacesInFillRun = countSpaces(run.text);
runX += runWidth + spacesInFillRun * wordSpacing;
}
currentY += currentLineHeight;
}
}
}
/**
* Sets fill and stroke styles on the canvas context for text rendering.
* @param context - The canvas context
* @param style - The text style
* @param metrics - The text metrics for gradient calculation
* @param padding - The padding value
* @param halfStroke - Half the stroke width
* @param offsetX - X offset for gradient positioning
* @param offsetY - Y offset for gradient positioning
*/
_setFillAndStrokeStyles(context, style, metrics, padding, halfStroke, offsetX = 0, offsetY = 0) {
context.fillStyle = style._fill ? getCanvasFillStyle.getCanvasFillStyle(style._fill, context, metrics, padding * 2, offsetX, offsetY) : null;
if (style._stroke?.width) {
const strokePadding = halfStroke + padding * 2;
context.strokeStyle = getCanvasFillStyle.getCanvasFillStyle(
style._stroke,
context,
metrics,
strokePadding,
offsetX,
offsetY
);
}
}
/**
* Sets up the canvas context for drop shadow rendering.
* @param context - The canvas context
* @param style - The text style containing drop shadow options
* @param resolution - The resolution multiplier
* @param dsOffsetShadow - The shadow Y offset
*/
_setupDropShadow(context, style, resolution, dsOffsetShadow) {
context.fillStyle = "black";
context.strokeStyle = "black";
const shadowOptions = style.dropShadow;
const dropShadowColor = shadowOptions.color;
const dropShadowAlpha = shadowOptions.alpha;
context.shadowColor = Color.Color.shared.setValue(dropShadowColor).setAlpha(dropShadowAlpha).toRgbaString();
const dropShadowBlur = shadowOptions.blur * resolution;
const dropShadowDistance = shadowOptions.distance * resolution;
context.shadowBlur = dropShadowBlur;
context.shadowOffsetX = Math.cos(shadowOptions.angle) * dropShadowDistance;
context.shadowOffsetY = Math.sin(shadowOptions.angle) * dropShadowDistance + dsOffsetShadow;
}
/**
* Calculates the X offset for text alignment.
* @param lineWidth - The width of the current line
* @param alignWidth - The width to align against (maxLineWidth or wordWrapWidth)
* @param align - The text alignment
* @returns The X offset for this line
*/
_getAlignmentOffset(lineWidth, alignWidth, align) {
if (align === "right") {
return alignWidth - lineWidth;
} else if (align === "center") {
return (alignWidth - lineWidth) / 2;
}
return 0;
}
/**
* Render the text with letter-spacing.
*
* This method handles rendering text with the correct letter spacing, using either:
* 1. Native letter spacing if supported by the browser
* 2. Manual letter spacing calculation if not natively supported
*
* For manual letter spacing, it calculates the position of each character
* based on its width and the desired spacing.
* @param text - The text to draw
* @param style - The text style to apply
* @param canvasAndContext - The canvas and context to draw to
* @param x - Horizontal position to draw the text
* @param y - Vertical position to draw the text
* @param isStroke - Whether to render the stroke (true) or fill (false)
* @param wordSpacing - Extra spacing to add between words (for justify alignment)
* @private
*/
_drawLetterSpacing(text, style, canvasAndContext, x, y, isStroke = false, wordSpacing = 0) {
const { context } = canvasAndContext;
const letterSpacing = style.letterSpacing;
let useExperimentalLetterSpacing = false;
if (CanvasTextMetrics.CanvasTextMetrics.experimentalLetterSpacingSupported) {
if (CanvasTextMetrics.CanvasTextMetrics.experimentalLetterSpacing) {
context.letterSpacing = `${letterSpacing}px`;
context.textLetterSpacing = `${letterSpacing}px`;
useExperimentalLetterSpacing = true;
} else {
context.letterSpacing = "0px";
context.textLetterSpacing = "0px";
}
}
if ((letterSpacing === 0 || useExperimentalLetterSpacing) && wordSpacing === 0) {
if (isStroke) {
context.strokeText(text, x, y);
} else {
context.fillText(text, x, y);
}
return;
}
if (wordSpacing !== 0 && (letterSpacing === 0 || useExperimentalLetterSpacing)) {
const words = text.split(" ");
let currentPosition2 = x;
const spaceWidth = context.measureText(" ").width;
for (let i = 0; i < words.length; i++) {
if (isStroke) {
context.strokeText(words[i], currentPosition2, y);
} else {
context.fillText(words[i], currentPosition2, y);
}
currentPosition2 += context.measureText(words[i]).width + spaceWidth + wordSpacing;
}
return;
}
let currentPosition = x;
const stringArray = CanvasTextMetrics.CanvasTextMetrics.graphemeSegmenter(text);
let previousWidth = context.measureText(text).width;
let currentWidth = 0;
for (let i = 0; i < stringArray.length; ++i) {
const currentChar = stringArray[i];
if (isStroke) {
context.strokeText(currentChar, currentPosition, y);
} else {
context.fillText(currentChar, currentPosition, y);
}
let textStr = "";
for (let j = i + 1; j < stringArray.length; ++j) {
textStr += stringArray[j];
}
currentWidth = context.measureText(textStr).width;
currentPosition += previousWidth - currentWidth + letterSpacing;
if (currentChar === " ") currentPosition += wordSpacing;
previousWidth = currentWidth;
}
}
}
const CanvasTextGenerator = new CanvasTextGeneratorClass();
exports.CanvasTextGenerator = CanvasTextGenerator;
//# sourceMappingURL=CanvasTextGenerator.js.map