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

301 lines (298 loc) 12.6 kB
import { Matrix } from '../../../maths/matrix/Matrix.mjs'; import { Container } from '../../container/Container.mjs'; import { FillGradient } from '../../graphics/shared/fill/FillGradient.mjs'; import { CanvasTextGenerator } from '../canvas/CanvasTextGenerator.mjs'; import { CanvasTextMetrics } from '../canvas/CanvasTextMetrics.mjs'; import { Text } from '../Text.mjs'; "use strict"; function getAlignmentOffset(alignment, lineWidth, largestLine) { switch (alignment) { case "center": return (largestLine - lineWidth) / 2; case "right": return largestLine - lineWidth; case "left": default: return 0; } } function isNewlineCharacter(char) { return char === "\r" || char === "\n" || char === "\r\n"; } const whitespaceRegex = /^\s*$/; function groupTextSegments(segments, measuredText) { const groupedSegments = []; let currentLine = measuredText.lines[0]; let matchedLine = ""; let chars = []; let lineCount = 0; segments.forEach((segment) => { const isWhitespace = whitespaceRegex.test(segment); const isNewline = isNewlineCharacter(segment); const isSpaceAtStart = matchedLine.length === 0 && isWhitespace; if (isWhitespace && !isNewline && isSpaceAtStart) { return; } if (!isNewline) matchedLine += segment; chars.push(segment); if (matchedLine.length >= currentLine.length) { groupedSegments.push({ line: matchedLine, chars }); chars = []; matchedLine = ""; lineCount++; currentLine = measuredText.lines[lineCount]; } }); return groupedSegments; } function canvasTextSplit(options) { const { text, style, chars: existingChars } = options; const textStyle = style; const measuredText = CanvasTextMetrics.measureText(text, textStyle); if (measuredText.runsByLine && measuredText.runsByLine.length > 0) { return canvasTaggedTextSplitFromRuns(measuredText, textStyle, existingChars, text); } const segments = CanvasTextMetrics.graphemeSegmenter(text); const groupedSegments = groupTextSegments(segments, measuredText); const alignment = textStyle.align; const maxLineWidth = measuredText.lineWidths.reduce((max, line) => Math.max(max, line), 0); const isSingleLine = measuredText.lines.length === 1; const useWordWrapWidth = !isSingleLine && textStyle.wordWrap; const alignWidth = useWordWrapWidth ? Math.max(textStyle.wordWrapWidth, maxLineWidth) : maxLineWidth; const fillGradient = textStyle._fill?.fill; const strokeGradient = textStyle._stroke?.fill; const hasFillGradient = fillGradient instanceof FillGradient; const hasStrokeGradient = strokeGradient instanceof FillGradient; const hasGradient = hasFillGradient || hasStrokeGradient; const hasLocalGradient = hasFillGradient && fillGradient.textureSpace === "local" || hasStrokeGradient && strokeGradient.textureSpace === "local"; const fullTextWidth = measuredText.width; const fullTextHeight = measuredText.height; const baseCharStyle = textStyle.clone(); baseCharStyle.align = "left"; let trimOffsetX = 0; let trimOffsetY = 0; if (baseCharStyle.trim) { const { frame, canvasAndContext } = CanvasTextGenerator.getCanvasAndContext({ text, style: textStyle, resolution: 1 }); CanvasTextGenerator.returnCanvasAndContext(canvasAndContext); trimOffsetX = -frame.x; trimOffsetY = -frame.y; baseCharStyle.trim = false; } const chars = []; const lineContainers = []; const wordContainers = []; let yOffset = 0; let existingCharIndex = 0; const gradientBounds = hasLocalGradient ? { width: fullTextWidth, height: fullTextHeight } : null; groupedSegments.forEach((group, lineIndex) => { const lineContainer = new Container({ label: `line-${lineIndex}` }); lineContainer.y = yOffset + trimOffsetY; lineContainers.push(lineContainer); const lineWidth = measuredText.lineWidths[lineIndex]; let xOffset = getAlignmentOffset(alignment, lineWidth, alignWidth); let currentWordContainer = new Container({ label: "word" }); currentWordContainer.x = xOffset + trimOffsetX; const context = CanvasTextMetrics._context; context.font = baseCharStyle._fontString; if (CanvasTextMetrics.experimentalLetterSpacingSupported) { context.letterSpacing = "0px"; context.textLetterSpacing = "0px"; } let remainingLineText = group.line; let previousRemainingWidth = context.measureText(remainingLineText).width; group.chars.forEach((segment) => { if (isNewlineCharacter(segment)) { return; } remainingLineText = remainingLineText.slice(segment.length); const currentRemainingWidth = remainingLineText.length > 0 ? context.measureText(remainingLineText).width : 0; const charAdvance = previousRemainingWidth - currentRemainingWidth; previousRemainingWidth = currentRemainingWidth; if (charAdvance === 0) return; if (segment === " ") { if (currentWordContainer.children.length > 0) { wordContainers.push(currentWordContainer); lineContainer.addChild(currentWordContainer); } xOffset += charAdvance + textStyle.letterSpacing; currentWordContainer = new Container({ label: "word" }); currentWordContainer.x = xOffset + trimOffsetX; } else { let charStyle = baseCharStyle; if (hasGradient) { charStyle = baseCharStyle.clone(); charStyle._gradientOffset = { x: -xOffset, y: -yOffset }; if (gradientBounds) { charStyle._gradientBounds = gradientBounds; } } let char; if (existingCharIndex < existingChars.length) { char = existingChars[existingCharIndex++]; char.text = segment; char.style = charStyle; char.setFromMatrix(Matrix.IDENTITY); char.x = xOffset - currentWordContainer.x + trimOffsetX; } else { char = new Text({ text: segment, style: charStyle, x: xOffset - currentWordContainer.x + trimOffsetX }); } chars.push(char); currentWordContainer.addChild(char); xOffset += charAdvance + textStyle.letterSpacing; } }); if (currentWordContainer.children.length > 0) { wordContainers.push(currentWordContainer); lineContainer.addChild(currentWordContainer); } if (alignment === "justify" && textStyle.wordWrap && lineIndex < groupedSegments.length - 1) { const lineWords = lineContainer.children; const wordGaps = lineWords.length - 1; if (wordGaps > 0) { const extraPerGap = (alignWidth - lineWidth) / wordGaps; for (let i = 1; i < lineWords.length; i++) { lineWords[i].x += i * extraPerGap; } } } yOffset += measuredText.lineHeight; }); return { chars, lines: lineContainers, words: wordContainers }; } function canvasTaggedTextSplitFromRuns(measuredText, textStyle, existingChars, text) { const { runsByLine } = measuredText; const alignment = textStyle.align; const maxLineWidth = measuredText.lineWidths.reduce((max, line) => Math.max(max, line), 0); const isSingleLine = measuredText.lines.length === 1; const useWordWrapWidth = !isSingleLine && textStyle.wordWrap; const alignWidth = useWordWrapWidth ? Math.max(textStyle.wordWrapWidth, maxLineWidth) : maxLineWidth; let trimOffsetX = 0; let trimOffsetY = 0; if (textStyle.trim) { const { frame, canvasAndContext } = CanvasTextGenerator.getCanvasAndContext({ text, style: textStyle, resolution: 1 }); CanvasTextGenerator.returnCanvasAndContext(canvasAndContext); trimOffsetX = -frame.x; trimOffsetY = -frame.y; } const chars = []; const lineContainers = []; const wordContainers = []; let yOffset = 0; let existingCharIndex = 0; runsByLine.forEach((lineRuns, lineIndex) => { const lineContainer = new Container({ label: `line-${lineIndex}` }); lineContainer.y = yOffset + trimOffsetY; lineContainers.push(lineContainer); const lineWidth = measuredText.lineWidths[lineIndex]; let xOffset = getAlignmentOffset(alignment, lineWidth, alignWidth); let currentWordContainer = new Container({ label: "word" }); currentWordContainer.x = xOffset + trimOffsetX; for (const run of lineRuns) { const runStyle = run.style; const fillGradient = runStyle._fill?.fill; const strokeGradient = runStyle._stroke?.fill; const hasFillGradient = fillGradient instanceof FillGradient; const hasStrokeGradient = strokeGradient instanceof FillGradient; const hasGradient = hasFillGradient || hasStrokeGradient; const hasLocalGradient = hasFillGradient && fillGradient.textureSpace === "local" || hasStrokeGradient && strokeGradient.textureSpace === "local"; const graphemes = CanvasTextMetrics.graphemeSegmenter(run.text); const baseRunStyle = runStyle.clone(); baseRunStyle.align = "left"; baseRunStyle.wordWrap = false; if (baseRunStyle.trim) baseRunStyle.trim = false; baseRunStyle.tagStyles = void 0; const context = CanvasTextMetrics._context; context.font = baseRunStyle._fontString; if (CanvasTextMetrics.experimentalLetterSpacingSupported) { context.letterSpacing = "0px"; context.textLetterSpacing = "0px"; } let remainingText = run.text; let previousRemainingWidth = context.measureText(remainingText).width; const runStartX = xOffset; const runTextWidth = previousRemainingWidth; const runFontProps = CanvasTextMetrics.measureFont(baseRunStyle._fontString); const runHeight = runStyle.lineHeight || runFontProps.fontSize; const runGradientBounds = hasLocalGradient ? { width: runTextWidth, height: runHeight } : null; for (const grapheme of graphemes) { remainingText = remainingText.slice(grapheme.length); const currentRemainingWidth = remainingText.length > 0 ? context.measureText(remainingText).width : 0; const charAdvance = previousRemainingWidth - currentRemainingWidth; previousRemainingWidth = currentRemainingWidth; if (isNewlineCharacter(grapheme)) continue; if (charAdvance === 0) continue; if (grapheme === " ") { if (currentWordContainer.children.length > 0) { wordContainers.push(currentWordContainer); lineContainer.addChild(currentWordContainer); } xOffset += charAdvance + runStyle.letterSpacing; currentWordContainer = new Container({ label: "word" }); currentWordContainer.x = xOffset + trimOffsetX; } else { let charStyle = baseRunStyle; if (hasGradient) { charStyle = baseRunStyle.clone(); if (hasLocalGradient) { charStyle._gradientOffset = { x: -(xOffset - runStartX), y: 0 }; charStyle._gradientBounds = runGradientBounds; } else { charStyle._gradientOffset = { x: -(xOffset - runStartX), y: 0 }; } } let char; if (existingCharIndex < existingChars.length) { char = existingChars[existingCharIndex++]; char.text = grapheme; char.style = charStyle; char.setFromMatrix(Matrix.IDENTITY); char.x = xOffset - currentWordContainer.x + trimOffsetX; } else { char = new Text({ text: grapheme, style: charStyle, x: xOffset - currentWordContainer.x + trimOffsetX }); } chars.push(char); currentWordContainer.addChild(char); xOffset += charAdvance + runStyle.letterSpacing; } } } if (currentWordContainer.children.length > 0) { wordContainers.push(currentWordContainer); lineContainer.addChild(currentWordContainer); } if (alignment === "justify" && textStyle.wordWrap && lineIndex < runsByLine.length - 1) { const lineWords = lineContainer.children; const wordGaps = lineWords.length - 1; if (wordGaps > 0) { const extraPerGap = (alignWidth - lineWidth) / wordGaps; for (let i = 1; i < lineWords.length; i++) { lineWords[i].x += i * extraPerGap; } } } const lineHeight = measuredText.lineHeights?.[lineIndex] ?? measuredText.lineHeight; yOffset += lineHeight; }); return { chars, lines: lineContainers, words: wordContainers }; } export { canvasTextSplit }; //# sourceMappingURL=canvasTextSplit.mjs.map