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">

494 lines (490 loc) 19.8 kB
'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