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

206 lines (203 loc) 8.8 kB
import { Color } from '../../../color/Color.mjs'; import { Rectangle } from '../../../maths/shapes/Rectangle.mjs'; import { CanvasPool } from '../../../rendering/renderers/shared/texture/CanvasPool.mjs'; import { getCanvasBoundingBox } from '../../../utils/canvas/getCanvasBoundingBox.mjs'; import { CanvasTextMetrics } from './CanvasTextMetrics.mjs'; import { fontStringFromTextStyle } from './utils/fontStringFromTextStyle.mjs'; import { getCanvasFillStyle } from './utils/getCanvasFillStyle.mjs'; "use strict"; const tempRect = new Rectangle(); 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.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.getOptimalCanvasAndContext(width, height); this._renderTextToCanvas(text, style, padding, resolution, canvasAndContext); const frame = style.trim ? 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.returnCanvasAndContext(canvasAndContext); } /** * Renders text to its canvas, and updates its texture. * @param text - The text to render * @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 */ _renderTextToCanvas(text, style, padding, resolution, canvasAndContext) { const { canvas, context } = canvasAndContext; const font = fontStringFromTextStyle(style); const measured = CanvasTextMetrics.measureText(text || " ", 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; 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) { context.fillStyle = "black"; context.strokeStyle = "black"; const shadowOptions = style.dropShadow; const dropShadowColor = shadowOptions.color; const dropShadowAlpha = shadowOptions.alpha; context.shadowColor = 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; } else { context.fillStyle = style._fill ? getCanvasFillStyle(style._fill, context, measured) : null; if (style._stroke?.width) { const padding2 = style._stroke.width * style._stroke.alignment; context.strokeStyle = getCanvasFillStyle(style._stroke, context, measured, padding2); } context.shadowColor = "black"; } let linePositionYShift = (lineHeight - fontProperties.fontSize) / 2; if (lineHeight - fontProperties.fontSize < 0) { linePositionYShift = 0; } const strokeWidth = style._stroke?.width ?? 0; for (let i2 = 0; i2 < lines.length; i2++) { linePositionX = strokeWidth / 2; linePositionY = strokeWidth / 2 + i2 * lineHeight + fontProperties.ascent + linePositionYShift; if (style.align === "right") { linePositionX += maxLineWidth - lineWidths[i2]; } else if (style.align === "center") { linePositionX += (maxLineWidth - lineWidths[i2]) / 2; } if (style._stroke?.width) { this._drawLetterSpacing( lines[i2], style, canvasAndContext, linePositionX + padding, linePositionY + padding - dsOffsetText, true ); } if (style._fill !== void 0) { this._drawLetterSpacing( lines[i2], style, canvasAndContext, linePositionX + padding, linePositionY + padding - dsOffsetText ); } } } } /** * 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) * @private */ _drawLetterSpacing(text, style, canvasAndContext, x, y, isStroke = false) { const { context } = canvasAndContext; const letterSpacing = style.letterSpacing; 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"; } } if (letterSpacing === 0 || useExperimentalLetterSpacing) { if (isStroke) { context.strokeText(text, x, y); } else { context.fillText(text, x, y); } return; } let currentPosition = x; const stringArray = 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; previousWidth = currentWidth; } } } const CanvasTextGenerator = new CanvasTextGeneratorClass(); export { CanvasTextGenerator }; //# sourceMappingURL=CanvasTextGenerator.mjs.map