Skip to content

Commit

Permalink
feat(troika-three-text): initial support for multiple font files spli…
Browse files Browse the repository at this point in the history
…t by unicode range - #13

Mimics how CSS @font-face resolves across multiple fonts by unicode-range, only loading
those font files needed for the characters actually used. This allows for more complete
language coverage without having to include everything in one font file, and Latin text
can now load a smaller font file by default. The support is currently limited to an
internal set of fallback fonts, but it will be opened up to the public API soon.

Internally this required supporting varying font metrics between characters, which also
sets us up for inline styling e.g. varying font-size, font-style, etc. - see #65
  • Loading branch information
lojjic committed Dec 15, 2022
1 parent c6c71dc commit 726fa46
Show file tree
Hide file tree
Showing 5 changed files with 427 additions and 190 deletions.
3 changes: 3 additions & 0 deletions packages/troika-three-text/src/FontParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ function parserFactory(Typr, woff2otf) {
capHeight: firstNum(os2 && os2.sCapHeight, ascender),
xHeight: firstNum(os2 && os2.sxHeight, ascender),
lineGap: firstNum(os2 && os2.sTypoLineGap, hhea && hhea.lineGap),
supportsCodePoint(code) {
return Typr.U.codeToGlyph(typrFont, code) > 0
},
forEachGlyph(text, fontSize, letterSpacing, callback) {
let glyphX = 0
const fontScale = 1 / fontObj.unitsPerEm * fontSize
Expand Down
62 changes: 37 additions & 25 deletions packages/troika-three-text/src/TextBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import { createTypesetter } from './Typesetter.js'
import { generateSDF, warmUpSDFCanvas, resizeWebGLCanvasWithoutClearing } from './SDFGenerator.js'
import bidiFactory from 'bidi-js'
import fontParser from './FontParser.js'
import { fallbackFonts } from './fonts.js'


const CONFIG = {
defaultFontURL: 'https://fonts.gstatic.com/s/roboto/v18/KFOmCnqEu92Fr1Mu4mxM.woff', //Roboto Regular
defaultFontURL: null, // just uses fallbackFonts unless user configures a new default - this will change
sdfGlyphSize: 64,
sdfMargin: 1 / 16,
sdfExponent: 9,
textureWidth: 2048
textureWidth: 2048,
fallbackFonts
}
const tempColor = /*#__PURE__*/new Color()
let hasRequested = false
Expand Down Expand Up @@ -82,7 +84,7 @@ const atlases = Object.create(null)
* @property {Float32Array} glyphAtlasIndices - List holding each glyph's index in the SDF atlas.
* @property {Uint8Array} [glyphColors] - List holding each glyph's [r, g, b] color, if `colorRanges` was supplied.
* @property {Float32Array} [caretPositions] - A list of caret positions for all characters in the string; each is
* three elements: the starting X, the ending X, and the bottom Y for the caret.
* four elements: the starting X, the ending X, the bottom Y, and the top Y for the caret.
* @property {number} [caretHeight] - An appropriate height for all selection carets.
* @property {number} ascender - The font's ascender metric.
* @property {number} descender - The font's descender metric.
Expand Down Expand Up @@ -118,9 +120,17 @@ function getTextRenderInfo(args, callback) {
args = assign({}, args)
const totalStart = now()

// Apply default font here to avoid a 'null' atlas, and convert relative
// URLs to absolute so they can be resolved in the worker
args.font = toAbsoluteURL(args.font || CONFIG.defaultFontURL)
// Convert relative URL to absolute so it can be resolved in the worker, and add fallbacks.
// In the future we'll allow args.font to be a list with unicode ranges too.
const { defaultFontURL, fallbackFonts } = CONFIG
const fonts = fallbackFonts.slice()
if (defaultFontURL) {
fonts.push({label: 'default', src: toAbsoluteURL(defaultFontURL)})
}
if (args.font) {
fonts.push({label: 'user', src: toAbsoluteURL(args.font)})
}
args.font = fonts

// Normalize text to a string
args.text = '' + args.text
Expand Down Expand Up @@ -173,26 +183,32 @@ function getTextRenderInfo(args, callback) {
}

const {sdfTexture, sdfCanvas} = atlas
let fontGlyphs = atlas.glyphsByFont.get(args.font)
if (!fontGlyphs) {
atlas.glyphsByFont.set(args.font, fontGlyphs = new Map())
}

// Issue request to the typesetting engine in the worker
typesetInWorker(args).then(result => {
const {glyphIds, glyphPositions, fontSize, unitsPerEm, timings} = result
const {glyphIds, glyphFontIndices, fontData, glyphPositions, fontSize, timings} = result
const neededSDFs = []
const glyphBounds = new Float32Array(glyphIds.length * 4)
const fontSizeMult = fontSize / unitsPerEm
let boundsIdx = 0
let positionsIdx = 0
const quadsStart = now()

const fontGlyphMaps = fontData.map(font => {
let map = atlas.glyphsByFont.get(font.src)
if (!map) {
atlas.glyphsByFont.set(font.src, map = new Map())
}
return map
})

glyphIds.forEach((glyphId, i) => {
let glyphInfo = fontGlyphs.get(glyphId)
const fontIndex = glyphFontIndices[i]
const {src: fontSrc, unitsPerEm} = fontData[fontIndex]
let glyphInfo = fontGlyphMaps[fontIndex].get(glyphId)

// If this is a glyphId not seen before, add it to the atlas
if (!glyphInfo) {
const {path, pathBounds} = result.glyphData[glyphId]
const {path, pathBounds} = result.glyphData[fontSrc][glyphId]

// Margin around path edges in SDF, based on a percentage of the glyph's max dimension.
// Note we add an extra 0.5 px over the configured value because the outer 0.5 doesn't contain
Expand All @@ -207,7 +223,7 @@ function getTextRenderInfo(args, callback) {
pathBounds[2] + fontUnitsMargin,
pathBounds[3] + fontUnitsMargin,
]
fontGlyphs.set(glyphId, (glyphInfo = { path, atlasIndex, sdfViewBox }))
fontGlyphMaps[fontIndex].set(glyphId, (glyphInfo = { path, atlasIndex, sdfViewBox }))

// Collect those that need SDF generation
neededSDFs.push(glyphInfo)
Expand All @@ -218,6 +234,7 @@ function getTextRenderInfo(args, callback) {
const {sdfViewBox} = glyphInfo
const posX = glyphPositions[positionsIdx++]
const posY = glyphPositions[positionsIdx++]
const fontSizeMult = fontSize / unitsPerEm
glyphBounds[boundsIdx++] = posX + sdfViewBox[0] * fontSizeMult
glyphBounds[boundsIdx++] = posY + sdfViewBox[1] * fontSizeMult
glyphBounds[boundsIdx++] = posX + sdfViewBox[2] * fontSizeMult
Expand Down Expand Up @@ -266,7 +283,6 @@ function getTextRenderInfo(args, callback) {
glyphAtlasIndices: glyphIds,
glyphColors: result.glyphColors,
caretPositions: result.caretPositions,
caretHeight: result.caretHeight,
chunkedBounds: result.chunkedBounds,
ascender: result.ascender,
descender: result.descender,
Expand Down Expand Up @@ -449,15 +465,11 @@ const typesetInWorker = /*#__PURE__*/defineWorkerModule({
},
getTransferables(result) {
// Mark array buffers as transferable to avoid cloning during postMessage
const transferables = [
result.glyphPositions.buffer,
result.glyphIds.buffer
]
if (result.caretPositions) {
transferables.push(result.caretPositions.buffer)
}
if (result.glyphColors) {
transferables.push(result.glyphColors.buffer)
const transferables = []
for (let p in result) {
if (result[p] && result[p].buffer) {
transferables.push(result[p].buffer)
}
}
return transferables
}
Expand Down
Loading

0 comments on commit 726fa46

Please sign in to comment.