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