@lightningjs/renderer
Version:
Lightning 3 Renderer
282 lines • 9.99 kB
JavaScript
/*
* If not stated otherwise in this file or this component's LICENSE file the
* following copyright and licenses apply:
*
* Copyright 2023 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 {} from './types.js';
/**
* Returns CSS font setting string for use in canvas context.
*
* @param fontFace
* @param fontStyle
* @param fontSize
* @param precision
* @param defaultFontFace
* @returns
*/
export function getFontSetting(fontFace, fontStyle, fontSize, precision, defaultFontFace) {
let ff = fontFace;
if (!Array.isArray(ff)) {
ff = [ff];
}
const ffs = [];
for (let i = 0, n = ff.length; i < n; i++) {
let curFf = ff[i];
// Replace the default font face `null` with the actual default font face set
// on the stage.
if (curFf === null || curFf === undefined) {
curFf = defaultFontFace;
}
if (curFf === 'serif' || curFf === 'sans-serif') {
ffs.push(curFf);
}
else {
ffs.push(`"${curFf}"`);
}
}
return `${fontStyle} ${fontSize * precision}px ${ffs.join(',')}`;
}
/**
* Returns true if the given character is a zero-width space.
*
* @param space
*/
export function isZeroWidthSpace(space) {
return space === '' || space === '\u200B';
}
/**
* Returns true if the given character is a zero-width space or a regular space.
*
* @param space
*/
export function isSpace(space) {
return isZeroWidthSpace(space) || space === ' ';
}
/**
* Converts a string into an array of tokens and the words between them.
*
* @param tokenRegex
* @param text
*/
export function tokenizeString(tokenRegex, text) {
const delimeters = text.match(tokenRegex) || [];
const words = text.split(tokenRegex) || [];
const final = [];
for (let i = 0; i < words.length; i++) {
final.push(words[i], delimeters[i]);
}
final.pop();
return final.filter((word) => word != '');
}
/**
* Measure the width of a string accounting for letter spacing.
*
* @param context
* @param word
* @param space
*/
export function measureText(word, space = 0, context) {
if (!space) {
return context.measureText(word).width;
}
return word.split('').reduce((acc, char) => {
// Zero-width spaces should not include letter spacing.
// And since we know the width of a zero-width space is 0, we can skip
// measuring it.
if (isZeroWidthSpace(char)) {
return acc;
}
return acc + context.measureText(char).width + space;
}, 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 getWebFontMetrics(context, fontFace, fontSize) {
if (fontFace.metrics) {
return fontFace.metrics;
}
// 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 browserMetrics = context.measureText('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz');
console.warn(`Font metrics not provided for Canvas Web font ${fontFace.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.');
let metrics;
if (browserMetrics.actualBoundingBoxDescent &&
browserMetrics.actualBoundingBoxAscent) {
metrics = {
ascender: browserMetrics.actualBoundingBoxAscent / fontSize,
descender: -browserMetrics.actualBoundingBoxDescent / fontSize,
lineGap: 0.2,
};
}
else {
// If the browser doesn't support the font metrics API, we'll use some
// default values.
metrics = {
ascender: 0.8,
descender: -0.2,
lineGap: 0.2,
};
}
// Save the calculated metrics to the font face for future use.
fontFace.metrics = metrics;
return metrics;
}
/**
* Applies newlines to a string to have it optimally fit into the horizontal
* bounds set by the Text object's wordWrapWidth property.
*
* @param text
* @param wordWrapWidth
* @param suffix
* @param context
*/
export function wrapWord(word, wordWrapWidth, suffix, context) {
const suffixWidth = context.measureText(suffix).width;
const wordLen = word.length;
const wordWidth = context.measureText(word).width;
/* If word fits wrapWidth, do nothing */
if (wordWidth <= wordWrapWidth) {
return word;
}
/* Make initial guess for text cuttoff */
let cutoffIndex = Math.floor((wordWrapWidth * wordLen) / wordWidth);
let truncWordWidth = context.measureText(word.substring(0, cutoffIndex)).width +
suffixWidth;
/* In case guess was overestimated, shrink it letter by letter. */
if (truncWordWidth > wordWrapWidth) {
while (cutoffIndex > 0) {
truncWordWidth =
context.measureText(word.substring(0, cutoffIndex)).width +
suffixWidth;
if (truncWordWidth > wordWrapWidth) {
cutoffIndex -= 1;
}
else {
break;
}
}
/* In case guess was underestimated, extend it letter by letter. */
}
else {
while (cutoffIndex < wordLen) {
truncWordWidth =
context.measureText(word.substring(0, cutoffIndex)).width +
suffixWidth;
if (truncWordWidth < wordWrapWidth) {
cutoffIndex += 1;
}
else {
// Finally, when bound is crossed, retract last letter.
cutoffIndex -= 1;
break;
}
}
}
/* If wrapWidth is too short to even contain suffix alone, return empty string */
return (word.substring(0, cutoffIndex) +
(wordWrapWidth >= suffixWidth ? suffix : ''));
}
/**
* Applies newlines to a string to have it optimally fit into the horizontal
* bounds set by the Text object's wordWrapWidth property.
*/
export function wrapText(text, wordWrapWidth, letterSpacing, indent = 0, context) {
const spaceRegex = / |\u200B/g; // ZWSP and spaces
const lines = text.split(/\r?\n/g);
let allLines = [];
const realNewlines = [];
for (let i = 0; i < lines.length; i++) {
const resultLines = [];
let result = '';
let spaceLeft = wordWrapWidth - indent;
// Split the line into words, considering ZWSP
const words = lines[i].split(spaceRegex);
const spaces = lines[i].match(spaceRegex) || [];
for (let j = 0; j < words.length; j++) {
const space = spaces[j - 1] || '';
const word = words[j];
const wordWidth = measureText(word, letterSpacing, context);
const wordWidthWithSpace = isZeroWidthSpace(space)
? wordWidth
: wordWidth + measureText(space, letterSpacing, context);
if (j === 0 || wordWidthWithSpace > spaceLeft) {
if (j > 0) {
resultLines.push(result);
result = '';
}
result += word;
spaceLeft = wordWrapWidth - wordWidth - (j === 0 ? indent : 0);
}
else {
spaceLeft -= wordWidthWithSpace;
result += space + word;
}
}
resultLines.push(result);
result = '';
allLines = allLines.concat(resultLines);
if (i < lines.length - 1) {
realNewlines.push(allLines.length);
}
}
return { l: allLines, n: realNewlines };
}
/**
* Calculate height for the canvas
*
* @param textBaseline
* @param fontSize
* @param lineHeight
* @param numLines
* @param offsetY
* @returns
*/
export function calcHeight(textBaseline, fontSize, lineHeight, numLines) {
const baselineOffset = textBaseline !== 'bottom' ? 0.5 * fontSize : 0;
return (lineHeight * (numLines - 1) +
baselineOffset +
Math.max(lineHeight, fontSize));
}
//# sourceMappingURL=utils.js.map