@lightningjs/renderer
Version:
Lightning 3 Renderer
304 lines • 12.1 kB
JavaScript
/*
* If not stated otherwise in this file or this component's LICENSE file the
* following copyright and licenses apply:
*
* Copyright 2025 Comcast Cable Communications Management, LLC.
*
* Licensed under the Apache License, Version 2.0 (the License);
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { isZeroWidthSpace } from '../Utils.js';
import * as SdfFontHandler from '../SdfFontHandler.js';
export const measureLines = (lines, fontFamily, letterSpacing, fontScale, maxLines, hasMaxLines) => {
const measuredLines = [];
const designLetterSpacing = letterSpacing * fontScale;
let remainingLines = hasMaxLines === true ? maxLines : lines.length;
let i = 0;
while (remainingLines > 0) {
const line = lines[i];
if (line === undefined) {
continue;
}
const width = measureText(line, fontFamily, designLetterSpacing);
measuredLines.push([line, width]);
i++;
remainingLines--;
}
return [
measuredLines,
remainingLines,
hasMaxLines === true ? lines.length - measuredLines.length > 0 : false,
];
};
/**
* Wrap text for SDF rendering with proper width constraints
*/
export const wrapText = (text, fontFamily, fontScale, maxWidth, letterSpacing, overflowSuffix, wordBreak, maxLines, hasMaxLines) => {
const lines = text.split('\n');
const wrappedLines = [];
const maxWidthInDesignUnits = maxWidth / fontScale;
const designLetterSpacing = letterSpacing * fontScale;
// Calculate space width for line wrapping
const spaceWidth = measureText(' ', fontFamily, designLetterSpacing);
let wrappedLine = [];
let remainingLines = maxLines;
let hasRemainingText = true;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
[wrappedLine, remainingLines, hasRemainingText] = wrapLine(line, fontFamily, maxWidthInDesignUnits, designLetterSpacing, spaceWidth, overflowSuffix, wordBreak, remainingLines, hasMaxLines);
wrappedLines.push(...wrappedLine);
}
return [wrappedLines, remainingLines, hasRemainingText];
};
/**
* Wrap a single line of text for SDF rendering
*/
export const wrapLine = (line, fontFamily, maxWidth, designLetterSpacing, spaceWidth, overflowSuffix, wordBreak, remainingLines, hasMaxLines) => {
// Use the same space regex as Canvas renderer to handle ZWSP
const spaceRegex = / |\u200B/g;
const words = line.split(spaceRegex);
const spaces = line.match(spaceRegex) || [];
const wrappedLines = [];
let currentLine = '';
let currentLineWidth = 0;
let hasRemainingText = true;
let i = 0;
for (; i < words.length; i++) {
const word = words[i];
if (word === undefined) {
continue;
}
const space = spaces[i - 1] || '';
const wordWidth = measureText(word, fontFamily, designLetterSpacing);
// For width calculation, treat ZWSP as having 0 width but regular space functionality
const effectiveSpaceWidth = space === '\u200B' ? 0 : spaceWidth;
const totalWidth = currentLineWidth + effectiveSpaceWidth + wordWidth;
if ((i === 0 && wordWidth <= maxWidth) ||
(i > 0 && totalWidth <= maxWidth)) {
// Word fits on current line
if (currentLine.length > 0) {
// Add space - for ZWSP, don't add anything to output (it's invisible)
if (space !== '\u200B') {
currentLine += space;
currentLineWidth += effectiveSpaceWidth;
}
}
currentLine += word;
currentLineWidth += wordWidth;
}
else {
if (remainingLines === 1) {
if (currentLine.length > 0) {
// Add space - for ZWSP, don't add anything to output (it's invisible)
if (space !== '\u200B') {
currentLine += space;
currentLineWidth += effectiveSpaceWidth;
}
}
currentLine += word;
currentLineWidth += wordWidth;
remainingLines = 0;
hasRemainingText = i < words.length;
break;
}
if (wordBreak !== 'break-all' && currentLine.length > 0) {
wrappedLines.push([currentLine, currentLineWidth]);
currentLine = '';
currentLineWidth = 0;
remainingLines--;
}
if (wordBreak !== 'break-all') {
currentLine = word;
currentLineWidth = wordWidth;
}
if (wordBreak === 'break-word') {
const [lines, rl, rt] = breakWord(word, fontFamily, maxWidth, designLetterSpacing, remainingLines, hasMaxLines);
remainingLines = rl;
hasRemainingText = rt;
if (lines.length === 1) {
[currentLine, currentLineWidth] = lines[lines.length - 1];
}
else {
for (let j = 0; j < lines.length; j++) {
[currentLine, currentLineWidth] = lines[j];
if (j < lines.length - 1) {
wrappedLines.push(lines[j]);
}
}
}
}
else if (wordBreak === 'break-all') {
const codepoint = word.codePointAt(0);
const glyph = SdfFontHandler.getGlyph(fontFamily, codepoint);
const firstLetterWidth = glyph !== null ? glyph.xadvance + designLetterSpacing : 0;
let linebreak = false;
if (currentLineWidth + firstLetterWidth + effectiveSpaceWidth >
maxWidth) {
wrappedLines.push([currentLine, currentLineWidth]);
remainingLines -= 1;
currentLine = '';
currentLineWidth = 0;
linebreak = true;
}
const initial = maxWidth - currentLineWidth;
const [lines, rl, rt] = breakAll(word, fontFamily, initial, maxWidth, designLetterSpacing, remainingLines, hasMaxLines);
remainingLines = rl;
hasRemainingText = rt;
if (linebreak === false) {
const [text, width] = lines[0];
currentLine += ' ' + text;
currentLineWidth = width;
wrappedLines.push([currentLine, currentLineWidth]);
}
for (let j = 1; j < lines.length; j++) {
[currentLine, currentLineWidth] = lines[j];
if (j < lines.length - 1) {
wrappedLines.push([currentLine, currentLineWidth]);
}
}
}
}
}
// Add the last line if it has content
if (currentLine.length > 0 && remainingLines === 0) {
currentLine = truncateLineWithSuffix(currentLine, fontFamily, maxWidth, designLetterSpacing, overflowSuffix);
}
if (currentLine.length > 0) {
wrappedLines.push([currentLine, currentLineWidth]);
}
else {
wrappedLines.push(['', 0]);
}
return [wrappedLines, remainingLines, hasRemainingText];
};
/**
* Measure the width of text in SDF design units
*/
export const measureText = (text, fontFamily, designLetterSpacing) => {
let width = 0;
let prevCodepoint = 0;
for (let i = 0; i < text.length; i++) {
const char = text.charAt(i);
const codepoint = text.codePointAt(i);
if (codepoint === undefined)
continue;
// Skip zero-width spaces in width calculations
if (isZeroWidthSpace(char)) {
continue;
}
const glyph = SdfFontHandler.getGlyph(fontFamily, codepoint);
if (glyph === null)
continue;
let advance = glyph.xadvance;
// Add kerning if there's a previous character
if (prevCodepoint !== 0) {
const kerning = SdfFontHandler.getKerning(fontFamily, prevCodepoint, codepoint);
advance += kerning;
}
width += advance + designLetterSpacing;
prevCodepoint = codepoint;
}
return width;
};
/**
* Truncate a line with overflow suffix to fit within width
*/
export const truncateLineWithSuffix = (line, fontFamily, maxWidth, designLetterSpacing, overflowSuffix) => {
const suffixWidth = measureText(overflowSuffix, fontFamily, designLetterSpacing);
if (suffixWidth >= maxWidth) {
return overflowSuffix.substring(0, Math.max(1, overflowSuffix.length - 1));
}
let truncatedLine = line;
while (truncatedLine.length > 0) {
const lineWidth = measureText(truncatedLine, fontFamily, designLetterSpacing);
if (lineWidth + suffixWidth <= maxWidth) {
return truncatedLine + overflowSuffix;
}
truncatedLine = truncatedLine.substring(0, truncatedLine.length - 1);
}
return overflowSuffix;
};
/**
* wordbreak function: https://developer.mozilla.org/en-US/docs/Web/CSS/word-break#break-word
*/
export const breakWord = (word, fontFamily, maxWidth, designLetterSpacing, remainingLines, hasMaxLines) => {
const lines = [];
let currentPart = '';
let currentWidth = 0;
let i = 0;
for (let i = 0; i < word.length; i++) {
const char = word.charAt(i);
const codepoint = char.codePointAt(0);
if (codepoint === undefined)
continue;
const glyph = SdfFontHandler.getGlyph(fontFamily, codepoint);
if (glyph === null)
continue;
const charWidth = glyph.xadvance + designLetterSpacing;
if (currentWidth + charWidth > maxWidth && currentPart.length > 0) {
remainingLines--;
if (remainingLines === 0) {
break;
}
lines.push([currentPart, currentWidth]);
currentPart = char;
currentWidth = charWidth;
}
else {
currentPart += char;
currentWidth += charWidth;
}
}
if (currentPart.length > 0) {
lines.push([currentPart, currentWidth]);
}
return [lines, remainingLines, i < word.length - 1];
};
/**
* wordbreak function: https://developer.mozilla.org/en-US/docs/Web/CSS/word-break#break-word
*/
export const breakAll = (word, fontFamily, initial, maxWidth, designLetterSpacing, remainingLines, hasMaxLines) => {
const lines = [];
let currentPart = '';
let currentWidth = 0;
let max = initial;
let i = 0;
let hasRemainingText = false;
for (; i < word.length; i++) {
if (remainingLines === 0) {
hasRemainingText = true;
break;
}
const char = word.charAt(i);
const codepoint = char.codePointAt(0);
const glyph = SdfFontHandler.getGlyph(fontFamily, codepoint);
if (glyph === null)
continue;
const charWidth = glyph.xadvance + designLetterSpacing;
if (currentWidth + charWidth > max && currentPart.length > 0) {
lines.push([currentPart, currentWidth]);
currentPart = char;
currentWidth = charWidth;
max = maxWidth;
remainingLines--;
}
else {
currentPart += char;
currentWidth += charWidth;
}
}
if (currentPart.length > 0) {
lines.push([currentPart, currentWidth]);
}
return [lines, remainingLines, hasRemainingText];
};
//# sourceMappingURL=Utils.js.map