@lightningjs/renderer
Version:
Lightning 3 Renderer
391 lines • 13.4 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 { UpdateType } from '../CoreNode.js';
import { hasZeroWidthSpace } from './Utils.js';
import { normalizeFontMetrics } from './TextLayoutEngine.js';
//global state variables for SdfFontHandler
const fontCache = new Map();
const fontLoadPromises = new Map();
const normalizedMetrics = new Map();
const nodesWaitingForFont = Object.create(null);
let initialized = false;
/**
* Build kerning lookup table for fast access
* @param {Array} kernings - Kerning data from font
* @returns {KerningTable} Optimized kerning lookup table
*/
const buildKerningTable = (kernings) => {
const kerningTable = {};
let i = 0;
const length = kernings.length;
while (i < length) {
const kerning = kernings[i];
i++;
if (kerning === undefined) {
continue;
}
const second = kerning.second;
let firsts = kerningTable[second];
if (firsts === undefined) {
firsts = {};
kerningTable[second] = firsts;
}
firsts[kerning.first] = kerning.amount;
}
return kerningTable;
};
/**
* Build glyph map from font data for fast character lookup
* @param {Array} chars - Character data from font
* @returns {Map} Glyph map for character to glyph lookup
*/
const buildGlyphMap = (chars) => {
const glyphMap = new Map();
let maxCharHeight = 0;
let i = 0;
const length = chars.length;
while (i < length) {
const glyph = chars[i];
i++;
if (glyph === undefined) {
continue;
}
glyphMap.set(glyph.id, glyph);
const charHeight = glyph.yoffset + glyph.height;
if (charHeight > maxCharHeight) {
maxCharHeight = charHeight;
}
}
return glyphMap;
};
/**
* Process font data and create optimized cache entry
* @param {string} fontFamily - Font family name
* @param {SdfFontData} fontData - Raw font data
* @param {ImageTexture} atlasTexture - Atlas texture
* @param {FontMetrics} metrics - Font metrics
*/
const processFontData = (fontFamily, fontData, atlasTexture, metrics) => {
// Build optimized data structures
const glyphMap = buildGlyphMap(fontData.chars);
const kernings = buildKerningTable(fontData.kernings);
// Calculate max char height
let maxCharHeight = 0;
let i = 0;
const length = fontData.chars.length;
while (i < length) {
const glyph = fontData.chars[i];
if (glyph !== undefined) {
const charHeight = glyph.yoffset + glyph.height;
if (charHeight > maxCharHeight) {
maxCharHeight = charHeight;
}
}
i++;
}
if (metrics === undefined && fontData.lightningMetrics === undefined) {
console.warn(`Font metrics not found for SDF font ${fontFamily}. ` +
'Make sure you are using the latest version of the Lightning ' +
'3 msdf-generator tool to generate your SDF fonts. Using default metrics.');
}
metrics = metrics ||
fontData.lightningMetrics || {
ascender: 800,
descender: -200,
lineGap: 200,
unitsPerEm: 1000,
};
// Cache processed data
fontCache.set(fontFamily, {
data: fontData,
glyphMap,
kernings,
atlasTexture,
metrics,
maxCharHeight,
});
};
/**
* Check if the SDF font handler can render a font
* @param {TrProps} trProps - Text rendering properties
* @returns {boolean} True if the font can be rendered
*/
export const canRenderFont = (trProps) => {
return (isFontLoaded(trProps.fontFamily) || fontLoadPromises.has(trProps.fontFamily));
};
/**
* Load SDF font from JSON + PNG atlas
* @param {Object} options - Font loading options
* @param {string} options.fontFamily - Font family name
* @param {string} options.fontUrl - JSON font data URL (atlasDataUrl)
* @param {string} options.atlasUrl - PNG atlas texture URL
* @param {FontMetrics} options.metrics - Optional font metrics
*/
export const loadFont = async (stage, options) => {
const { fontFamily, atlasUrl, atlasDataUrl, metrics } = options;
// Early return if already loaded
if (fontCache.get(fontFamily) !== undefined) {
return;
}
// Early return if already loading
const existingPromise = fontLoadPromises.get(fontFamily);
if (existingPromise !== undefined) {
return existingPromise;
}
if (atlasDataUrl === undefined) {
throw new Error(`Atlas data URL must be provided for SDF font: ${fontFamily}`);
}
const nwff = (nodesWaitingForFont[fontFamily] = []);
// Create loading promise
const loadPromise = (async () => {
// Load font JSON data
const response = await fetch(atlasDataUrl);
if (!response.ok) {
throw new Error(`Failed to load font data: ${response.statusText}`);
}
const fontData = (await response.json());
if (!fontData || !fontData.chars) {
throw new Error('Invalid SDF font data format');
}
// Atlas texture should be provided externally
if (!atlasUrl) {
throw new Error('Atlas texture must be provided for SDF fonts');
}
// Wait for atlas texture to load
return new Promise((resolve, reject) => {
// create new atlas texture using ImageTexture
const atlasTexture = stage.txManager.createTexture('ImageTexture', {
src: atlasUrl,
premultiplyAlpha: false,
});
atlasTexture.setRenderableOwner(fontFamily, true);
atlasTexture.preventCleanup = true; // Prevent automatic cleanup
if (atlasTexture.state === 'loaded') {
// If already loaded, process immediately
processFontData(fontFamily, fontData, atlasTexture, metrics);
fontLoadPromises.delete(fontFamily);
for (let key in nwff) {
nwff[key].setUpdateType(UpdateType.Local);
}
delete nodesWaitingForFont[fontFamily];
return resolve();
}
atlasTexture.on('loaded', () => {
// Process and cache font data
processFontData(fontFamily, fontData, atlasTexture, metrics);
// remove from promises
fontLoadPromises.delete(fontFamily);
for (let key in nwff) {
nwff[key].setUpdateType(UpdateType.Local);
}
delete nodesWaitingForFont[fontFamily];
resolve();
});
atlasTexture.on('failed', (error) => {
// Cleanup on error
fontLoadPromises.delete(fontFamily);
if (fontCache[fontFamily]) {
delete fontCache[fontFamily];
}
console.error(`Failed to load SDF font: ${fontFamily}`, error);
reject(error);
});
});
})();
fontLoadPromises.set(fontFamily, loadPromise);
return loadPromise;
};
/**
* Stop waiting for a font to load
* @param {string} fontFamily - Font family name
* @param {CoreTextNode} node - Node that was waiting for the font
*/
export const waitingForFont = (fontFamily, node) => {
if (nodesWaitingForFont[fontFamily] === undefined) {
return;
}
nodesWaitingForFont[fontFamily][node.id] = node;
};
/**
* Stop waiting for a font to load
*
* @param fontFamily
* @param node
* @returns
*/
export const stopWaitingForFont = (fontFamily, node) => {
if (nodesWaitingForFont[fontFamily] === undefined) {
return;
}
delete nodesWaitingForFont[fontFamily][node.id];
};
/**
* Get the font families map for resolving fonts
*/
export const getFontFamilies = () => {
const families = {};
// SDF fonts don't use the traditional FontFamilyMap structure
// Return empty map since SDF fonts are handled differently
return families;
};
/**
* Initialize the SDF font handler
*/
export const init = (c) => {
if (initialized === true) {
return;
}
initialized = true;
};
export const type = 'sdf';
/**
* Check if a font is already loaded by font family
*/
export const isFontLoaded = (fontFamily) => {
return fontCache.has(fontFamily);
};
/**
* Get normalized font metrics for a font family
*/
export const getFontMetrics = (fontFamily, fontSize) => {
const out = normalizedMetrics.get(fontFamily);
if (out !== undefined) {
return out;
}
let metrics = fontCache.get(fontFamily).metrics;
return processFontMetrics(fontFamily, fontSize, metrics);
};
export const processFontMetrics = (fontFamily, fontSize, metrics) => {
const label = fontFamily + fontSize;
const normalized = normalizeFontMetrics(metrics, fontSize);
normalizedMetrics.set(label, normalized);
return normalized;
};
/**
* Get glyph data for a character in a specific font
* @param {string} fontFamily - Font family name
* @param {number} codepoint - Character codepoint
* @returns {Object|null} Glyph data or null if not found
*/
export const getGlyph = (fontFamily, codepoint) => {
const cache = fontCache.get(fontFamily);
if (cache === undefined)
return null;
return cache.glyphMap.get(codepoint) || cache.glyphMap.get(63) || null; // 63 = '?'
};
/**
* Get kerning value between two glyphs
* @param {string} fontFamily - Font family name
* @param {number} firstGlyph - First glyph ID
* @param {number} secondGlyph - Second glyph ID
* @returns {number} Kerning value or 0
*/
export const getKerning = (fontFamily, firstGlyph, secondGlyph) => {
const cache = fontCache.get(fontFamily);
if (cache === undefined)
return 0;
const seconds = cache.kernings[secondGlyph];
return seconds ? seconds[firstGlyph] || 0 : 0;
};
/**
* Get atlas texture for a font family
* @param {string} fontFamily - Font family name
* @returns {ImageTexture|null} Atlas texture or null
*/
export const getAtlas = (fontFamily) => {
const cache = fontCache.get(fontFamily);
return cache !== undefined ? cache.atlasTexture : null;
};
/**
* Get font data for a font family
* @param {string} fontFamily - Font family name
* @returns {SdfFontData|null} Font data or null
*/
export const getFontData = (fontFamily) => {
return fontCache.get(fontFamily);
};
/**
* Get maximum character height for a font family
* @param {string} fontFamily - Font family name
* @returns {number} Max character height or 0
*/
export const getMaxCharHeight = (fontFamily) => {
const cache = fontCache.get(fontFamily);
return cache !== undefined ? cache.maxCharHeight : 0;
};
/**
* Get all loaded font families
* @returns {string[]} Array of font family names
*/
export const getLoadedFonts = () => {
return Array.from(fontCache.keys());
};
/**
* Unload a font and free resources
* @param {string} fontFamily - Font family name
*/
export const unloadFont = (fontFamily) => {
const cache = fontCache.get(fontFamily);
if (cache !== undefined) {
// Free texture if needed
if (typeof cache.atlasTexture.free === 'function') {
cache.atlasTexture.free();
}
fontCache.delete(fontFamily);
}
};
export const measureText = (text, fontFamily, letterSpacing) => {
if (text.length === 1) {
const char = text.charAt(0);
const codepoint = text.codePointAt(0);
if (codepoint === undefined)
return 0;
if (hasZeroWidthSpace(char) === true)
return 0;
const glyph = getGlyph(fontFamily, codepoint);
if (glyph === null)
return 0;
return glyph.xadvance + letterSpacing;
}
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 (hasZeroWidthSpace(char)) {
continue;
}
const glyph = getGlyph(fontFamily, codepoint);
if (glyph === null)
continue;
let advance = glyph.xadvance;
// Add kerning if there's a previous character
if (prevCodepoint !== 0) {
const kerning = getKerning(fontFamily, prevCodepoint, codepoint);
advance += kerning;
}
width += advance + letterSpacing;
prevCodepoint = codepoint;
}
return width;
};
//# sourceMappingURL=SdfFontHandler.js.map