@lightningjs/renderer
Version:
Lightning 3 Renderer
355 lines • 13.7 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';
import { WebGlRenderer } from '../renderers/webgl/WebGlRenderer.js';
import { WebGlRenderOp } from '../renderers/webgl/WebGlRenderOp.js';
import { Sdf } from '../shaders/webgl/SdfShader.js';
import { BufferCollection } from '../renderers/webgl/internal/BufferCollection.js';
import { mergeColorAlpha } from '../../utils.js';
import { wrapText, measureLines } from './sdf/index.js';
// Each glyph requires 6 vertices (2 triangles) with 4 floats each (x, y, u, v)
const FLOATS_PER_VERTEX = 4;
const VERTICES_PER_GLYPH = 6;
// Type definition to match interface
const type = 'sdf';
let sdfShader = null;
// Initialize the SDF text renderer
const init = (stage) => {
SdfFontHandler.init();
// Register SDF shader with the shader manager
stage.shManager.registerShaderType('Sdf', Sdf);
sdfShader = stage.shManager.createShader('Sdf');
};
const font = SdfFontHandler;
/**
* SDF text renderer using MSDF/SDF fonts with WebGL
*
* @param stage - Stage instance for font resolution
* @param props - Text rendering properties
* @returns Object containing ImageData and dimensions
*/
const renderText = (stage, props) => {
// Early return if no text
if (props.text.length === 0) {
return {
width: 0,
height: 0,
};
}
// Get font cache for this font family
const fontData = SdfFontHandler.getFontData(props.fontFamily);
if (fontData === null) {
// Font not loaded, return empty result
return {
width: 0,
height: 0,
};
}
// Calculate text layout and generate glyph data for caching
const layout = generateTextLayout(props, fontData);
// For SDF renderer, ImageData is null since we render via WebGL
return {
width: layout.width,
height: layout.height,
layout, // Cache layout for addQuads
};
};
/**
* Add quads for rendering using cached layout data
*/
const addQuads = (layout) => {
if (layout === undefined) {
return null; // No layout data available
}
const glyphs = layout.glyphs;
const glyphsLength = glyphs.length;
if (glyphsLength === 0) {
return null;
}
const vertexBuffer = new Float32Array(glyphsLength * VERTICES_PER_GLYPH * FLOATS_PER_VERTEX);
let bufferIndex = 0;
let glyphIndex = 0;
while (glyphIndex < glyphsLength) {
const glyph = glyphs[glyphIndex];
glyphIndex++;
if (glyph === undefined) {
continue;
}
const x1 = glyph.x;
const y1 = glyph.y;
const x2 = x1 + glyph.width;
const y2 = y1 + glyph.height;
const u1 = glyph.atlasX;
const v1 = glyph.atlasY;
const u2 = u1 + glyph.atlasWidth;
const v2 = v1 + glyph.atlasHeight;
// Triangle 1: Top-left, top-right, bottom-left
// Vertex 1: Top-left
vertexBuffer[bufferIndex++] = x1;
vertexBuffer[bufferIndex++] = y1;
vertexBuffer[bufferIndex++] = u1;
vertexBuffer[bufferIndex++] = v1;
// Vertex 2: Top-right
vertexBuffer[bufferIndex++] = x2;
vertexBuffer[bufferIndex++] = y1;
vertexBuffer[bufferIndex++] = u2;
vertexBuffer[bufferIndex++] = v1;
// Vertex 3: Bottom-left
vertexBuffer[bufferIndex++] = x1;
vertexBuffer[bufferIndex++] = y2;
vertexBuffer[bufferIndex++] = u1;
vertexBuffer[bufferIndex++] = v2;
// Triangle 2: Top-right, bottom-right, bottom-left
// Vertex 4: Top-right (duplicate)
vertexBuffer[bufferIndex++] = x2;
vertexBuffer[bufferIndex++] = y1;
vertexBuffer[bufferIndex++] = u2;
vertexBuffer[bufferIndex++] = v1;
// Vertex 5: Bottom-right
vertexBuffer[bufferIndex++] = x2;
vertexBuffer[bufferIndex++] = y2;
vertexBuffer[bufferIndex++] = u2;
vertexBuffer[bufferIndex++] = v2;
// Vertex 6: Bottom-left (duplicate)
vertexBuffer[bufferIndex++] = x1;
vertexBuffer[bufferIndex++] = y2;
vertexBuffer[bufferIndex++] = u1;
vertexBuffer[bufferIndex++] = v2;
}
return vertexBuffer;
};
/**
* Create and submit WebGL render operations for SDF text
* This is called from CoreTextNode during rendering to add SDF text to the render pipeline
*/
const renderQuads = (renderer, layout, vertexBuffer, renderProps) => {
const fontFamily = renderProps.fontFamily;
const color = renderProps.color;
const offsetY = renderProps.offsetY;
const worldAlpha = renderProps.worldAlpha;
const globalTransform = renderProps.globalTransform;
const atlasTexture = SdfFontHandler.getAtlas(fontFamily);
if (atlasTexture === null) {
console.warn(`SDF atlas texture not found for font: ${fontFamily}`);
return;
}
// We can safely assume this is a WebGL renderer else this wouldn't be called
const glw = renderer.glw;
const stride = 4 * Float32Array.BYTES_PER_ELEMENT;
const webGlBuffer = glw.createBuffer();
if (!webGlBuffer) {
console.warn('Failed to create WebGL buffer for SDF text');
return;
}
const webGlBuffers = new BufferCollection([
{
buffer: webGlBuffer,
attributes: {
a_position: {
name: 'a_position',
size: 2,
type: glw.FLOAT,
normalized: false,
stride,
offset: 0,
},
a_textureCoords: {
name: 'a_textureCoords',
size: 2,
type: glw.FLOAT,
normalized: false,
stride,
offset: 2 * Float32Array.BYTES_PER_ELEMENT,
},
},
},
]);
const buffer = webGlBuffers.getBuffer('a_position');
if (buffer !== undefined) {
glw.arrayBufferData(buffer, vertexBuffer, glw.STATIC_DRAW);
}
const renderOp = new WebGlRenderOp(renderer, {
sdfShaderProps: {
transform: globalTransform,
color: mergeColorAlpha(color, worldAlpha),
size: layout.fontScale, // Use proper font scaling in shader
scrollY: offsetY || 0,
distanceRange: layout.distanceRange,
debug: false, // Disable debug mode
},
sdfBuffers: webGlBuffers,
shader: sdfShader,
alpha: worldAlpha,
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
clippingRect: renderProps.clippingRect,
height: layout.height,
width: layout.width,
rtt: false,
parentHasRenderTexture: renderProps.parentHasRenderTexture,
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
framebufferDimensions: renderProps.framebufferDimensions,
}, 0);
// Add atlas texture and set quad count
renderOp.addTexture(atlasTexture.ctxTexture);
renderOp.numQuads = layout.glyphs.length;
renderer.addRenderOp(renderOp);
};
/**
* Generate complete text layout with glyph positioning for caching
*/
const generateTextLayout = (props, fontData) => {
const commonFontData = fontData.common;
const text = props.text;
const fontSize = props.fontSize;
const letterSpacing = props.letterSpacing;
const fontFamily = props.fontFamily;
const textAlign = props.textAlign;
const maxWidth = props.maxWidth;
const maxHeight = props.maxHeight;
const maxLines = props.maxLines;
const overflowSuffix = props.overflowSuffix;
const wordBreak = props.wordBreak;
// Use the font's design size for proper scaling
const designLineHeight = commonFontData.lineHeight;
const designFontSize = fontData.info.size;
const lineHeight = props.lineHeight || (designLineHeight * fontSize) / designFontSize;
const atlasWidth = commonFontData.scaleW;
const atlasHeight = commonFontData.scaleH;
// Calculate the pixel scale from design units to pixels
const finalScale = fontSize / designFontSize;
// Calculate design letter spacing
const designLetterSpacing = (letterSpacing * designFontSize) / fontSize;
// Determine text wrapping behavior based on contain mode
const shouldWrapText = maxWidth > 0;
const heightConstraint = maxHeight > 0;
// Calculate maximum lines constraint from height if needed
let effectiveMaxLines = maxLines;
if (heightConstraint === true) {
const maxLinesFromHeight = Math.floor(maxHeight / (lineHeight * finalScale));
if (effectiveMaxLines === 0 || maxLinesFromHeight < effectiveMaxLines) {
effectiveMaxLines = maxLinesFromHeight;
}
}
const hasMaxLines = effectiveMaxLines > 0;
// Split text into lines based on wrapping constraints
const [lines, remainingLines, remainingText] = shouldWrapText
? wrapText(text, fontFamily, finalScale, maxWidth, letterSpacing, overflowSuffix, wordBreak, effectiveMaxLines, hasMaxLines)
: measureLines(text.split('\n'), fontFamily, letterSpacing, finalScale, effectiveMaxLines, hasMaxLines);
const glyphs = [];
let maxWidthFound = 0;
let currentY = 0;
for (let i = 0; i < lines.length; i++) {
if (lines[i][1] > maxWidthFound) {
maxWidthFound = lines[i][1];
}
}
// Second pass: Generate glyph layouts with proper alignment
let lineIndex = 0;
const linesLength = lines.length;
while (lineIndex < linesLength) {
const [line, lineWidth] = lines[lineIndex];
lineIndex++;
// Calculate line X offset based on text alignment
let lineXOffset = 0;
if (textAlign === 'center') {
const availableWidth = shouldWrapText
? maxWidth / finalScale
: maxWidthFound;
lineXOffset = (availableWidth - lineWidth) / 2;
}
else if (textAlign === 'right') {
const availableWidth = shouldWrapText
? maxWidth / finalScale
: maxWidthFound;
lineXOffset = availableWidth - lineWidth;
}
let currentX = lineXOffset;
let charIndex = 0;
const lineLength = line.length;
let prevCodepoint = 0;
while (charIndex < lineLength) {
const char = line.charAt(charIndex);
const codepoint = char.codePointAt(0);
charIndex++;
if (codepoint === undefined) {
continue;
}
// Skip zero-width spaces for rendering but keep them in the text flow
if (isZeroWidthSpace(char)) {
continue;
}
// Get glyph data from font handler
const glyph = SdfFontHandler.getGlyph(fontFamily, codepoint);
if (glyph === null) {
continue;
}
// Calculate advance with kerning (in design units)
let advance = glyph.xadvance;
// Add kerning if there's a previous character
if (prevCodepoint !== 0) {
const kerning = SdfFontHandler.getKerning(fontFamily, prevCodepoint, codepoint);
advance += kerning;
}
// Calculate glyph position and atlas coordinates (in design units)
const glyphLayout = {
codepoint,
glyphId: glyph.id,
x: currentX + glyph.xoffset,
y: currentY + glyph.yoffset,
width: glyph.width,
height: glyph.height,
xOffset: glyph.xoffset,
yOffset: glyph.yoffset,
atlasX: glyph.x / atlasWidth,
atlasY: glyph.y / atlasHeight,
atlasWidth: glyph.width / atlasWidth,
atlasHeight: glyph.height / atlasHeight,
};
glyphs.push(glyphLayout);
// Advance position with letter spacing (in design units)
currentX += advance + designLetterSpacing;
prevCodepoint = codepoint;
}
currentY += designLineHeight;
}
// Convert final dimensions to pixel space for the layout
return {
glyphs,
distanceRange: finalScale * fontData.distanceField.distanceRange,
width: Math.ceil(maxWidthFound * finalScale),
height: Math.ceil(designLineHeight * lines.length * finalScale),
fontScale: finalScale,
lineHeight,
fontFamily,
};
};
/**
* SDF Text Renderer - implements TextRenderer interface
*/
const SdfTextRenderer = {
type,
font,
renderText,
addQuads,
renderQuads,
init,
};
export default SdfTextRenderer;
//# sourceMappingURL=SdfTextRenderer.js.map