UNPKG

@lightningjs/renderer

Version:
295 lines (259 loc) 8.8 kB
/* * 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;