@lightningjs/renderer
Version:
Lightning 3 Renderer
224 lines • 8.12 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 { hasZeroWidthSpace } from './Utils.js';
import { UpdateType } from '../CoreNode.js';
import { defaultFontMetrics, normalizeFontMetrics, } from './TextLayoutEngine.js';
/**
* Global font set regardless of if run in the main thread or a web worker
*/
// const globalFontSet: FontFaceSet = (resolvedGlobal.document?.fonts ||
// (resolvedGlobal as unknown as { fonts: FontFaceSet }).fonts) as FontFaceSet;
// Global state variables for fontHandler
const fontFamilies = {};
const fontLoadPromises = new Map();
const normalizedMetrics = new Map();
const nodesWaitingForFont = Object.create(null);
const fontCache = new Map();
let initialized = false;
let context;
let measureContext;
/**
* Check if a font can be rendered
*/
export const canRenderFont = () => {
// Canvas can always render any font family (assuming the browser supports it)
return true;
};
const processFontData = (fontFamily, fontFace, metrics) => {
metrics = metrics || defaultFontMetrics;
fontCache.set(fontFamily, {
fontFamily,
fontFace,
metrics,
});
};
/**
* Load a font by providing fontFamily, fontUrl, and optional metrics
*/
export const loadFont = async (stage, options) => {
const { fontFamily, fontUrl, metrics } = options;
// If already loaded, return immediately
if (fontCache.has(fontFamily) === true) {
return;
}
const existingPromise = fontLoadPromises.get(fontFamily);
// If already loading, return the existing promise
if (existingPromise !== undefined) {
return existingPromise;
}
const nwff = (nodesWaitingForFont[fontFamily] = []);
// Create and store the loading promise
const loadPromise = new FontFace(fontFamily, `url(${fontUrl})`)
.load()
.then((loadedFont) => {
stage.platform.addFont(loadedFont);
processFontData(fontFamily, loadedFont, metrics);
fontLoadPromises.delete(fontFamily);
for (let key in nwff) {
nwff[key].setUpdateType(UpdateType.Local);
}
delete nodesWaitingForFont[fontFamily];
})
.catch((error) => {
fontLoadPromises.delete(fontFamily);
console.error(`Failed to load font: ${fontFamily}`, error);
throw error;
});
fontLoadPromises.set(fontFamily, loadPromise);
return loadPromise;
};
/**
* Get the font families map for resolving fonts
*/
export const getFontFamilies = () => {
return fontFamilies;
};
/**
* Initialize the global font handler
*/
export const init = (c, mc) => {
if (initialized === true) {
return;
}
if (c === undefined) {
throw new Error('Canvas context is not provided for font handler initialization');
}
context = c;
measureContext = mc;
// Register the default 'sans-serif' font face
const defaultMetrics = {
ascender: 800,
descender: -200,
lineGap: 200,
unitsPerEm: 1000,
};
processFontData('sans-serif', undefined, defaultMetrics);
initialized = true;
};
export const type = 'canvas';
/**
* Check if a font is already loaded by font family
*/
export const isFontLoaded = (fontFamily) => {
return fontCache.has(fontFamily);
};
/**
* Wait for a font to load
*
* @param fontFamily
* @param node
*/
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];
};
export const getFontMetrics = (fontFamily, fontSize) => {
const out = normalizedMetrics.get(fontFamily + fontSize);
if (out !== undefined) {
return out;
}
let metrics = fontCache.get(fontFamily).metrics;
if (metrics === undefined) {
metrics = calculateFontMetrics(fontFamily, fontSize);
}
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;
};
export const measureText = (text, fontFamily, letterSpacing) => {
if (letterSpacing === 0) {
return measureContext.measureText(text).width;
}
if (hasZeroWidthSpace(text) === false) {
return measureContext.measureText(text).width + letterSpacing * text.length;
}
return text.split('').reduce((acc, char) => {
if (hasZeroWidthSpace(char) === true) {
return acc;
}
return acc + measureContext.measureText(char).width + letterSpacing;
}, 0);
};
/**
* Get the font metrics for a font face.
*
* @remarks
* This function will attempt to grab the explicitly defined metrics from the
* font face first. If the font face does not have metrics defined, it will
* attempt to calculate the metrics using the browser's measureText method.
*
* If the browser does not support the font metrics API, it will use some
* default values.
*
* @param context
* @param fontFace
* @param fontSize
* @returns
*/
export function calculateFontMetrics(fontFamily, fontSize) {
// If the font face doesn't have metrics defined, we fallback to using the
// browser's measureText method to calculate take a best guess at the font
// actual font's metrics.
// - fontBoundingBox[Ascent|Descent] is the best estimate but only supported
// in Chrome 87+ (2020), Firefox 116+ (2023), and Safari 11.1+ (2018).
// - It is an estimate as it can vary between browsers.
// - actualBoundingBox[Ascent|Descent] is less accurate and supported in
// Chrome 77+ (2019), Firefox 74+ (2020), and Safari 11.1+ (2018).
// - If neither are supported, we'll use some default values which will
// get text on the screen but likely not be great.
// NOTE: It's been decided not to rely on fontBoundingBox[Ascent|Descent]
// as it's browser support is limited and it also tends to produce higher than
// expected values. It is instead HIGHLY RECOMMENDED that developers provide
// explicit metrics in the font face definition.
const metrics = measureContext.measureText('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz');
console.warn(`Font metrics not provided for Canvas Web font ${fontFamily}. ` +
'Using fallback values. It is HIGHLY recommended you use the latest ' +
'version of the Lightning 3 `msdf-generator` tool to extract the default ' +
'metrics for the font and provide them in the Canvas Web font definition.');
const ascender = metrics.fontBoundingBoxAscent ?? metrics.actualBoundingBoxAscent ?? 0;
const descender = metrics.fontBoundingBoxDescent ?? metrics.actualBoundingBoxDescent ?? 0;
return {
ascender,
descender: -descender,
lineGap: (metrics.emHeightAscent ?? 0) +
(metrics.emHeightDescent ?? 0) -
(ascender + descender),
unitsPerEm: (metrics.emHeightAscent ?? 0) + (metrics.emHeightDescent ?? 0),
};
}
//# sourceMappingURL=CanvasFontHandler.js.map