'use strict'; var Color = require('../../../color/Color.js'); var Extensions = require('../../../extensions/Extensions.js'); var pow2 = require('../../../maths/misc/pow2.js'); var CanvasPool = require('../../../rendering/renderers/shared/texture/CanvasPool.js'); var TexturePool = require('../../../rendering/renderers/shared/texture/TexturePool.js'); var getCanvasBoundingBox = require('../../../utils/canvas/getCanvasBoundingBox.js'); var deprecation = require('../../../utils/logging/deprecation.js'); var TextStyle = require('../TextStyle.js'); var getPo2TextureFromSource = require('../utils/getPo2TextureFromSource.js'); var CanvasTextMetrics = require('./CanvasTextMetrics.js'); var fontStringFromTextStyle = require('./utils/fontStringFromTextStyle.js'); var getCanvasFillStyle = require('./utils/getCanvasFillStyle.js'); "use strict"; class CanvasTextSystem { constructor(_renderer) { this._activeTextures = {}; this._renderer = _renderer; } getTextureSize(text, resolution, style) { const measured = CanvasTextMetrics.CanvasTextMetrics.measureText(text || " ", style); let width = Math.ceil(Math.ceil(Math.max(1, measured.width) + style.padding * 2) * resolution); let height = Math.ceil(Math.ceil(Math.max(1, measured.height) + style.padding * 2) * resolution); width = Math.ceil(width - 1e-6); height = Math.ceil(height - 1e-6); width = pow2.nextPow2(width); height = pow2.nextPow2(height); return { width, height }; } getTexture(options, resolution, style, _textKey) { if (typeof options === "string") { deprecation.deprecation("8.0.0", "CanvasTextSystem.getTexture: Use object TextOptions instead of separate arguments"); options = { text: options, style, resolution }; } if (!(options.style instanceof TextStyle.TextStyle)) { options.style = new TextStyle.TextStyle(options.style); } const { texture, canvasAndContext } = this.createTextureAndCanvas( options ); this._renderer.texture.initSource(texture._source); CanvasPool.CanvasPool.returnCanvasAndContext(canvasAndContext); return texture; } createTextureAndCanvas(options) { const { text, style } = options; const resolution = options.resolution ?? this._renderer.resolution; const measured = CanvasTextMetrics.CanvasTextMetrics.measureText(text || " ", style); const width = Math.ceil(Math.ceil(Math.max(1, measured.width) + style.padding * 2) * resolution); const height = Math.ceil(Math.ceil(Math.max(1, measured.height) + style.padding * 2) * resolution); const canvasAndContext = CanvasPool.CanvasPool.getOptimalCanvasAndContext(width, height); const { canvas } = canvasAndContext; this.renderTextToCanvas(text, style, resolution, canvasAndContext); const texture = getPo2TextureFromSource.getPo2TextureFromSource(canvas, width, height, resolution); if (style.trim) { const trimmed = getCanvasBoundingBox.getCanvasBoundingBox(canvas, resolution); texture.frame.copyFrom(trimmed); texture.updateUvs(); } return { texture, canvasAndContext }; } getManagedTexture(text) { text._resolution = text._autoResolution ? this._renderer.resolution : text.resolution; const textKey = text._getKey(); if (this._activeTextures[textKey]) { this._increaseReferenceCount(textKey); return this._activeTextures[textKey].texture; } const { texture, canvasAndContext } = this.createTextureAndCanvas(text); this._activeTextures[textKey] = { canvasAndContext, texture, usageCount: 1 }; return texture; } _increaseReferenceCount(textKey) { this._activeTextures[textKey].usageCount++; } decreaseReferenceCount(textKey) { const activeTexture = this._activeTextures[textKey]; activeTexture.usageCount--; if (activeTexture.usageCount === 0) { CanvasPool.CanvasPool.returnCanvasAndContext(activeTexture.canvasAndContext); TexturePool.TexturePool.returnTexture(activeTexture.texture); const source = activeTexture.texture.source; source.resource = null; source.uploadMethodId = "unknown"; source.alphaMode = "no-premultiply-alpha"; this._activeTextures[textKey] = null; } } getReferenceCount(textKey) { return this._activeTextures[textKey].usageCount; } /** * Renders text to its canvas, and updates its texture. * * By default this is used internally to ensure the texture is correct before rendering, * but it can be used called externally, for example from this class to 'pre-generate' the texture from a piece of text, * and then shared across multiple Sprites. * @param text * @param style * @param resolution * @param canvasAndContext */ renderTextToCanvas(text, style, resolution, canvasAndContext) { const { canvas, context } = canvasAndContext; const font = fontStringFromTextStyle.fontStringFromTextStyle(style); const measured = CanvasTextMetrics.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); const padding = style.padding * 2; context.clearRect(0, 0, measured.width + 4 + padding, measured.height + 4 + padding); 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) + style.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.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.globalAlpha = style._fill?.alpha ?? 1; context.fillStyle = style._fill ? getCanvasFillStyle.getCanvasFillStyle(style._fill, context) : null; if (style._stroke?.width) { context.strokeStyle = getCanvasFillStyle.getCanvasFillStyle(style._stroke, context); } 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 + style.padding, linePositionY + style.padding - dsOffsetText, true ); } if (style._fill !== void 0) { this._drawLetterSpacing( lines[i2], style, canvasAndContext, linePositionX + style.padding, linePositionY + style.padding - dsOffsetText ); } } } } /** * Render the text with letter-spacing. * @param text - The text to draw * @param style * @param canvasAndContext * @param x - Horizontal position to draw the text * @param y - Vertical position to draw the text * @param isStroke - Is this drawing for the outside stroke of the * text? If not, it's for the inside fill */ _drawLetterSpacing(text, style, canvasAndContext, x, y, isStroke = false) { 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) { if (isStroke) { context.strokeText(text, x, y); } else { context.fillText(text, x, y); } 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; previousWidth = currentWidth; } } destroy() { this._activeTextures = null; } } /** @ignore */ CanvasTextSystem.extension = { type: [ Extensions.ExtensionType.WebGLSystem, Extensions.ExtensionType.WebGPUSystem, Extensions.ExtensionType.CanvasSystem ], name: "canvasText" }; exports.CanvasTextSystem = CanvasTextSystem; //# sourceMappingURL=CanvasTextSystem.js.map