Initial project import
This commit is contained in:
276
node_modules/troika-three-text/src/FontResolver.js
generated
vendored
Normal file
276
node_modules/troika-three-text/src/FontResolver.js
generated
vendored
Normal file
@@ -0,0 +1,276 @@
|
||||
import fontParser from "./FontParser.js";
|
||||
import unicodeFontResolverClientFactory from "../libs/unicode-font-resolver-client.factory.js";
|
||||
import { defineWorkerModule } from "troika-worker-utils";
|
||||
|
||||
/**
|
||||
* @typedef {string | {src:string, label?:string, unicodeRange?:string, lang?:string}} UserFont
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {ClientOptions} FontResolverOptions
|
||||
* @property {Array<UserFont>|UserFont} [fonts]
|
||||
* @property {'normal'|'italic'} [style]
|
||||
* @property {'normal'|'bold'|number} [style]
|
||||
* @property {string} [unicodeFontsURL]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} FontResolverResult
|
||||
* @property {Uint8Array} chars
|
||||
* @property {Array<ParsedFont & {src:string}>} fonts
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {function} FontResolver
|
||||
* @param {string} text
|
||||
* @param {(FontResolverResult) => void} callback
|
||||
* @param {FontResolverOptions} [options]
|
||||
*/
|
||||
|
||||
/**
|
||||
* Factory for the FontResolver function.
|
||||
* @param {FontParser} fontParser
|
||||
* @param {{getFontsForString: function, CodePointSet: function}} unicodeFontResolverClient
|
||||
* @return {FontResolver}
|
||||
*/
|
||||
export function createFontResolver(fontParser, unicodeFontResolverClient) {
|
||||
/**
|
||||
* @type {Record<string, ParsedFont>}
|
||||
*/
|
||||
const parsedFonts = Object.create(null)
|
||||
|
||||
/**
|
||||
* @type {Record<string, Array<(ParsedFont) => void>>}
|
||||
*/
|
||||
const loadingFonts = Object.create(null)
|
||||
|
||||
/**
|
||||
* Load a given font url
|
||||
*/
|
||||
function doLoadFont(url, callback) {
|
||||
const onError = err => {
|
||||
console.error(`Failure loading font ${url}`, err)
|
||||
}
|
||||
try {
|
||||
const request = new XMLHttpRequest()
|
||||
request.open('get', url, true)
|
||||
request.responseType = 'arraybuffer'
|
||||
request.onload = function () {
|
||||
if (request.status >= 400) {
|
||||
onError(new Error(request.statusText))
|
||||
}
|
||||
else if (request.status > 0) {
|
||||
try {
|
||||
const fontObj = fontParser(request.response)
|
||||
fontObj.src = url;
|
||||
callback(fontObj)
|
||||
} catch (e) {
|
||||
onError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
request.onerror = onError
|
||||
request.send()
|
||||
} catch(err) {
|
||||
onError(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load a given font url if needed, invoking a callback when it's loaded. If already
|
||||
* loaded, the callback will be called synchronously.
|
||||
* @param {string} fontUrl
|
||||
* @param {(font: ParsedFont) => void} callback
|
||||
*/
|
||||
function loadFont(fontUrl, callback) {
|
||||
let font = parsedFonts[fontUrl]
|
||||
if (font) {
|
||||
callback(font)
|
||||
} else if (loadingFonts[fontUrl]) {
|
||||
loadingFonts[fontUrl].push(callback)
|
||||
} else {
|
||||
loadingFonts[fontUrl] = [callback]
|
||||
doLoadFont(fontUrl, fontObj => {
|
||||
fontObj.src = fontUrl
|
||||
parsedFonts[fontUrl] = fontObj
|
||||
loadingFonts[fontUrl].forEach(cb => cb(fontObj))
|
||||
delete loadingFonts[fontUrl];
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For a given string of text, determine which fonts are required to fully render it and
|
||||
* ensure those fonts are loaded.
|
||||
*/
|
||||
return function (text, callback, {
|
||||
lang,
|
||||
fonts: userFonts = [],
|
||||
style = 'normal',
|
||||
weight = 'normal',
|
||||
unicodeFontsURL
|
||||
} = {}) {
|
||||
const charResolutions = new Uint8Array(text.length);
|
||||
const fontResolutions = [];
|
||||
if (!text.length) {
|
||||
allDone()
|
||||
}
|
||||
|
||||
const fontIndices = new Map();
|
||||
const fallbackRanges = [] // [[start, end], ...]
|
||||
|
||||
if (style !== 'italic') style = 'normal'
|
||||
if (typeof weight !== 'number') {
|
||||
weight = weight === 'bold' ? 700 : 400
|
||||
}
|
||||
|
||||
if (userFonts && !Array.isArray(userFonts)) {
|
||||
userFonts = [userFonts]
|
||||
}
|
||||
userFonts = userFonts.slice()
|
||||
// filter by language
|
||||
.filter(def => !def.lang || def.lang.test(lang))
|
||||
// switch order for easier iteration
|
||||
.reverse()
|
||||
if (userFonts.length) {
|
||||
const UNKNOWN = 0
|
||||
const RESOLVED = 1
|
||||
const NEEDS_FALLBACK = 2
|
||||
let prevCharResult = UNKNOWN
|
||||
|
||||
;(function resolveUserFonts (startIndex = 0) {
|
||||
for (let i = startIndex, iLen = text.length; i < iLen; i++) {
|
||||
const codePoint = text.codePointAt(i)
|
||||
// Carry previous character's result forward if:
|
||||
// - it resolved to a font that also covers this character
|
||||
// - this character is whitespace
|
||||
if (
|
||||
(prevCharResult === RESOLVED && fontResolutions[charResolutions[i - 1]].supportsCodePoint(codePoint)) ||
|
||||
(i > 0 && /\s/.test(text[i]))
|
||||
) {
|
||||
charResolutions[i] = charResolutions[i - 1]
|
||||
if (prevCharResult === NEEDS_FALLBACK) {
|
||||
fallbackRanges[fallbackRanges.length - 1][1] = i
|
||||
}
|
||||
} else {
|
||||
for (let j = charResolutions[i], jLen = userFonts.length; j <= jLen; j++) {
|
||||
if (j === jLen) {
|
||||
// none of the user fonts matched; needs fallback
|
||||
const range = prevCharResult === NEEDS_FALLBACK ?
|
||||
fallbackRanges[fallbackRanges.length - 1] :
|
||||
(fallbackRanges[fallbackRanges.length] = [i, i])
|
||||
range[1] = i;
|
||||
prevCharResult = NEEDS_FALLBACK;
|
||||
} else {
|
||||
charResolutions[i] = j;
|
||||
const { src, unicodeRange } = userFonts[j];
|
||||
// filter by optional explicit unicode ranges
|
||||
if (!unicodeRange || isCodeInRanges(codePoint, unicodeRange)) {
|
||||
const fontObj = parsedFonts[src];
|
||||
// font not yet loaded, load it and resume
|
||||
if (!fontObj) {
|
||||
loadFont(src, () => {
|
||||
resolveUserFonts(i);
|
||||
});
|
||||
return;
|
||||
}
|
||||
// if the font actually contains a glyph for this char, lock it in
|
||||
if (fontObj.supportsCodePoint(codePoint)) {
|
||||
let fontIndex = fontIndices.get(fontObj);
|
||||
if (typeof fontIndex !== 'number') {
|
||||
fontIndex = fontResolutions.length;
|
||||
fontResolutions.push(fontObj);
|
||||
fontIndices.set(fontObj, fontIndex);
|
||||
}
|
||||
charResolutions[i] = fontIndex;
|
||||
prevCharResult = RESOLVED;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (codePoint > 0xffff && i + 1 < iLen) {
|
||||
charResolutions[i + 1] = charResolutions[i]
|
||||
i++
|
||||
if (prevCharResult === NEEDS_FALLBACK) {
|
||||
fallbackRanges[fallbackRanges.length - 1][1] = i
|
||||
}
|
||||
}
|
||||
}
|
||||
resolveFallbacks();
|
||||
})();
|
||||
} else {
|
||||
fallbackRanges.push([0, text.length - 1])
|
||||
resolveFallbacks();
|
||||
}
|
||||
|
||||
function resolveFallbacks() {
|
||||
if (fallbackRanges.length) {
|
||||
// Combine all fallback substrings into a single string for querying
|
||||
const fallbackString = fallbackRanges.map(range => text.substring(range[0], range[1] + 1)).join('\n')
|
||||
unicodeFontResolverClient.getFontsForString(fallbackString, {
|
||||
lang: lang || undefined,
|
||||
style,
|
||||
weight,
|
||||
dataUrl: unicodeFontsURL
|
||||
}).then(({fontUrls, chars}) => {
|
||||
// Extract results and put them back in the main array
|
||||
const fontIndexOffset = fontResolutions.length
|
||||
let charIdx = 0;
|
||||
fallbackRanges.forEach(range => {
|
||||
for (let i = 0, endIdx = range[1] - range[0]; i <= endIdx; i++) {
|
||||
charResolutions[range[0] + i] = chars[charIdx++] + fontIndexOffset
|
||||
}
|
||||
charIdx++ //skip segment separator
|
||||
})
|
||||
|
||||
// Load and parse the fallback fonts - avoiding Promise here to prevent polyfills in the worker
|
||||
let loadedCount = 0;
|
||||
fontUrls.forEach((url, i) => {
|
||||
loadFont(url, fontObj => {
|
||||
fontResolutions[i + fontIndexOffset] = fontObj
|
||||
if (++loadedCount === fontUrls.length) {
|
||||
allDone();
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
} else {
|
||||
allDone();
|
||||
}
|
||||
}
|
||||
|
||||
function allDone() {
|
||||
callback({
|
||||
chars: charResolutions,
|
||||
fonts: fontResolutions
|
||||
})
|
||||
}
|
||||
|
||||
function isCodeInRanges(code, ranges) {
|
||||
// todo optimize search - CodePointSet from unicode-font-resolver?
|
||||
for (let k = 0; k < ranges.length; k++) {
|
||||
const [start, end = start] = ranges[k]
|
||||
if (start <= code && code <= end) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const fontResolverWorkerModule = /*#__PURE__*/defineWorkerModule({
|
||||
name: 'FontResolver',
|
||||
dependencies: [
|
||||
createFontResolver,
|
||||
fontParser,
|
||||
unicodeFontResolverClientFactory,
|
||||
],
|
||||
init(createFontResolver, fontParser, unicodeFontResolverClientFactory) {
|
||||
return createFontResolver(fontParser, unicodeFontResolverClientFactory());
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user