@lightningjs/renderer
Version:
Lightning 3 Renderer
295 lines (259 loc) • 8.8 kB
text/typescript
/*
* 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 type { Stage } from '../Stage.js';
import type {
FontHandler,
SdfRenderInfo,
TextLineStruct,
TextRenderInfo,
} from './TextRenderer.js';
import type { CoreTextNode, CoreTextNodeProps } from '../CoreTextNode.js';
import { getLayoutCacheKey, hasZeroWidthSpace } from './Utils.js';
import * as SdfFontHandler from './SdfFontHandler.js';
import { WebGlRenderer } from '../renderers/webgl/WebGlRenderer.js';
import { Sdf } from '../shaders/webgl/SdfShader.js';
import type { WebGlShaderNode } from '../renderers/webgl/WebGlShaderNode.js';
import type { TextLayout } from './TextRenderer.js';
import { mapTextLayout } from './TextLayoutEngine.js';
import type { WebGlCtxTexture } from '../renderers/webgl/WebGlCtxTexture.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' as const;
let sdfShader: WebGlShaderNode | null = null;
let renderer: WebGlRenderer | null = null;
// Initialize the SDF text renderer
const init = (stage: Stage): void => {
SdfFontHandler.init();
// Register SDF shader with the shader manager
stage.shManager.registerShaderType('Sdf', Sdf);
sdfShader = stage.shManager.createShader('Sdf') as WebGlShaderNode;
renderer = stage.renderer as WebGlRenderer;
};
const font: FontHandler = SdfFontHandler;
const renderInfoCache = new Map<string, SdfRenderInfo>();
/**
* 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 = (props: CoreTextNodeProps): TextRenderInfo => {
const cacheKey = getLayoutCacheKey(props);
let renderInfo = renderInfoCache.get(cacheKey);
if (renderInfo !== undefined) {
return renderInfo;
}
// Calculate text layout and generate glyph data for caching
const layout = generateTextLayout(
props,
SdfFontHandler.getFontData(props.fontFamily)!,
);
renderInfo = {
type,
layout,
width: layout.width,
height: layout.height,
remainingLines: layout.remainingLines,
hasRemainingText: layout.hasRemainingText,
atlasTexture: SdfFontHandler.getAtlas(props.fontFamily)!
.ctxTexture as WebGlCtxTexture,
} as SdfRenderInfo;
renderInfoCache.set(cacheKey, renderInfo);
// For SDF renderer, ImageData is null since we render via WebGL
return renderInfo;
};
/**
* 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 = (textNode: CoreTextNode): void => {
textNode.props.shader = sdfShader;
renderer!.addRenderOp(textNode);
};
/**
* Generate complete text layout with glyph positioning for caching
*/
const generateTextLayout = (
props: CoreTextNodeProps,
fontCache: SdfFontHandler.SdfFont,
): TextLayout => {
const fontSize = props.fontSize;
const fontFamily = props.fontFamily;
const lineHeight = props.lineHeight;
const metrics = SdfFontHandler.getFontMetrics(fontFamily, fontSize);
const fontData = fontCache.data;
const commonFontData = fontData.common;
const designFontSize = fontData.info.size;
const atlasWidth = commonFontData.scaleW;
const atlasHeight = commonFontData.scaleH;
// Calculate the pixel scale from design units to pixels
const fontScale = fontSize / designFontSize;
const letterSpacing = props.letterSpacing / fontScale;
const maxWidth = props.maxWidth / fontScale;
const maxHeight = props.maxHeight;
const [
lines,
remainingLines,
hasRemainingText,
bareLineHeight,
lineHeightPx,
effectiveWidth,
effectiveHeight,
] = mapTextLayout(
SdfFontHandler.measureText,
metrics,
props.text,
props.textAlign,
fontFamily,
lineHeight,
props.overflowSuffix,
props.wordBreak,
letterSpacing,
props.maxLines,
maxWidth,
maxHeight,
);
const lineAmount = lines.length;
let bufferIndex = 0;
let glyphCount = 0;
// Count total glyphs (excluding spaces) for buffer allocation
for (let i = 0; i < lineAmount; i++) {
const textLine = (lines[i] as TextLineStruct)[0];
for (const char of textLine) {
if (hasZeroWidthSpace(char) === true) {
continue;
}
const codepoint = char.codePointAt(0);
if (codepoint === undefined) {
continue;
}
glyphCount++;
}
}
const vertexBuffer = new Float32Array(
glyphCount * VERTICES_PER_GLYPH * FLOATS_PER_VERTEX,
);
let currentX = 0;
let currentY = 0;
for (let i = 0; i < lineAmount; i++) {
const line = lines[i] as TextLineStruct;
const textLine = line[0];
let prevGlyphId = 0;
currentX = line[3];
//convert Y coord to vertex value
currentY = line[4] / fontScale;
for (const char of textLine) {
if (hasZeroWidthSpace(char) === true) {
continue;
}
const codepoint = char.codePointAt(0);
if (codepoint === undefined) {
continue;
}
// Get glyph data from font handler
const glyph = SdfFontHandler.getGlyph(fontFamily, codepoint);
if (glyph === null) {
continue;
}
// Kerning offsets the current glyph relative to the previous glyph.
let kerning = 0;
// Add kerning if there's a previous character
if (prevGlyphId !== 0) {
kerning = SdfFontHandler.getKerning(fontFamily, prevGlyphId, glyph.id);
}
// Apply pair kerning before placing this glyph.
currentX += kerning;
const x1 = currentX + glyph.xoffset;
const y1 = currentY + glyph.yoffset;
const x2 = x1 + glyph.width;
const y2 = y1 + glyph.height;
const u1 = glyph.x / atlasWidth;
const v1 = glyph.y / atlasHeight;
const u2 = u1 + glyph.width / atlasWidth;
const v2 = v1 + glyph.height / 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;
// Advance position with letter spacing (in design units)
currentX += glyph.xadvance + letterSpacing;
prevGlyphId = glyph.id;
}
currentY += lineHeightPx;
}
// Convert final dimensions to pixel space for the layout
return {
vertexBuffer,
glyphCount,
distanceRange: fontScale * fontData.distanceField.distanceRange,
width: effectiveWidth * fontScale,
height: effectiveHeight,
fontScale: fontScale,
lineHeight: lineHeightPx,
fontFamily,
remainingLines,
hasRemainingText,
};
};
const clearCache = (): void => {
renderInfoCache.clear();
};
/**
* SDF Text Renderer - implements TextRenderer interface
*/
const SdfTextRenderer = {
type,
font,
renderText,
renderQuads,
init,
clearCache,
};
export default SdfTextRenderer;