UNPKG

@lightningjs/renderer

Version:
282 lines 9.99 kB
/* * 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