UNPKG

@lightningjs/renderer

Version:
355 lines 13.7 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 { 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