Skip to content

Commit

Permalink
feat(troika-3d-text): add preloadFont utility
Browse files Browse the repository at this point in the history
This allows preloading of font files and optional pregeneration of glyph
SDFs for sets of characters. Closes issue #39. Also added some timings
to the font processor result.
  • Loading branch information
lojjic committed Apr 20, 2020
1 parent b58f7b9 commit acedd3c
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 8 deletions.
26 changes: 26 additions & 0 deletions packages/troika-3d-text/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,29 @@ Note that `'normal'` in this context _does_ honor newline characters to manually

Default: `'normal'`


## Preloading

To avoid long pauses when first displaying a piece of text in your scene, you can preload fonts and optionally pre-generate the SDF textures for particular glyphs up front:

```js
import {preloadFont} from 'troika-3d-text'

myApp.showLoadingScreen()

preloadFont(
'path/to/myfontfile.woff',
'abcdefghijklmnopqrstuvwxyz',
() => {
myApp.showScene()
}
)
```

The arguments are:

- `font` - The URL of the font file to preload. If `null` is passed, this will preload the default font.

- `charSequences` - A string or array of string character sequences for which to pre-generate glyph SDF textures. Note that this _will_ honor ligature substitution, so you may need to specify ligature sequences in addition to their individual characters to get all possible glyphs, e.g. `["t", "h", "th"]` to get the "t" and "h" glyphs plus the "th" glyph.

- `callback` - A function that will be called when the preloading is complete.
21 changes: 20 additions & 1 deletion packages/troika-3d-text/src/FontProcessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ export function createFontProcessor(fontParser, sdfGenerator, config) {
callback,
metricsOnly=false
) {
const mainStart = now()
const timings = {total: 0, fontLoad: 0, layout: 0, sdf: {}, sdfTotal: 0}

// Ensure newlines are normalized
if (text.indexOf('\r') > -1) {
console.warn('FontProcessor.process: got text with \\r chars; normalizing to \\n')
Expand All @@ -194,6 +197,8 @@ export function createFontProcessor(fontParser, sdfGenerator, config) {
let maxLineWidth = 0
let canWrap = whiteSpace !== 'nowrap'
const {ascender, descender, unitsPerEm} = fontObj
timings.fontLoad = now() - mainStart
const layoutStart = now()

// Find conversion between native font units and fontSize units; this will already be done
// for the gx/gy values below but everything else we'll need to convert
Expand Down Expand Up @@ -369,7 +374,9 @@ export function createFontProcessor(fontParser, sdfGenerator, config) {
// If we haven't seen this glyph yet, generate its SDF
let glyphAtlasInfo = atlas.glyphs[glyphObj.index]
if (!glyphAtlasInfo) {
const sdfStart = now()
const glyphSDFData = sdfGenerator(glyphObj)
timings.sdf[glyphInfo.char] = now() - sdfStart

// Assign this glyph the next available atlas index
glyphSDFData.atlasIndex = atlas.glyphCount++
Expand Down Expand Up @@ -468,6 +475,13 @@ export function createFontProcessor(fontParser, sdfGenerator, config) {
})
}

// Timing stats
for (let ch in timings.sdf) {
timings.sdfTotal += timings.sdf[ch]
}
timings.layout = now() - layoutStart - timings.sdfTotal
timings.total = now() - mainStart

callback({
glyphBounds, //rendering quad bounds for each glyph [x1, y1, x2, y2]
glyphAtlasIndices, //atlas indices for each glyph
Expand All @@ -480,7 +494,8 @@ export function createFontProcessor(fontParser, sdfGenerator, config) {
topBaseline, //y coordinate of the top line's baseline
totalBounds, //total rect including all glyphBounds; will be slightly larger than glyph edges due to SDF padding
totalBlockSize: [maxLineWidth, lines.length * lineHeight], //width and height of the text block; accurate for layout measurement
newGlyphSDFs: newGlyphs //if this request included any new SDFs for the atlas, they'll be included here
newGlyphSDFs: newGlyphs, //if this request included any new SDFs for the atlas, they'll be included here
timings
})
})
}
Expand All @@ -507,6 +522,10 @@ export function createFontProcessor(fontParser, sdfGenerator, config) {
return isNaN(pct) ? 0 : pct / 100
}

function now() {
return (self.performance || Date).now()
}

return {
process,
measure,
Expand Down
41 changes: 36 additions & 5 deletions packages/troika-3d-text/src/TextBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ let hasRequested = false
* 2048^2 allows for 1024 glyphs.) This can be increased if you need to increase the
* glyph size and/or have an extraordinary number of glyphs.
*/
export function configureTextBuilder(config) {
function configureTextBuilder(config) {
if (hasRequested) {
console.warn('configureTextBuilder called after first font request; will be ignored.')
} else {
Expand Down Expand Up @@ -87,6 +87,8 @@ const atlases = Object.create(null)
* extra SDF padding so it is accurate to use for measurement.
* @property {Array<number>} chunkedBounds - List of bounding rects for each consecutive set of N glyphs,
* in the format `{start:N, end:N, rect:[minX, minY, maxX, maxY]}`.
* @property {object} timings - Timing info for various parts of the rendering logic including SDF
* generation, layout, etc.
* @frozen
*/

Expand All @@ -101,7 +103,7 @@ const atlases = Object.create(null)
* @param {object} args
* @param {getTextRenderInfo~callback} callback
*/
export function getTextRenderInfo(args, callback) {
function getTextRenderInfo(args, callback) {
args = assign({}, args)

// Apply default font here to avoid a 'null' atlas, and convert relative
Expand Down Expand Up @@ -178,11 +180,32 @@ export function getTextRenderInfo(args, callback) {
lineHeight: result.lineHeight,
topBaseline: result.topBaseline,
totalBounds: result.totalBounds,
totalBlockSize: result.totalBlockSize
totalBlockSize: result.totalBlockSize,
timings: result.timings
}))
})
}


/**
* Preload a given font and optionally pre-generate glyph SDFs for one or more character sequences.
* This can be useful to avoid long pauses when first showing text in a scene, by preloading the
* needed fonts and glyphs up front along with other assets.
*
* @param {string} font - URL of the font file to preload. If not given, the default font will
* be loaded.
* @param {string|string[]} charSequences - One or more character sequences for which to pre-
* generate glyph SDFs. Note that this will honor ligature substitution, so you may need
* to specify ligature sequences in addition to their individual characters to get all
* possible glyphs, e.g. `["t", "h", "th"]` to get the "t" and "h" glyphs plus the "th" ligature.
* @param {function} callback - A function that will be called when the preloading is complete.
*/
function preloadFont(font, charSequences, callback) {
let text = Array.isArray(charSequences) ? charSequences.join('\n') : '' + charSequences
getTextRenderInfo({ font, text }, callback)
}


// Local assign impl so we don't have to import troika-core
function assign(toObj, fromObj) {
for (let key in fromObj) {
Expand All @@ -194,7 +217,7 @@ function assign(toObj, fromObj) {
}


export const fontProcessorWorkerModule = defineWorkerModule({
const fontProcessorWorkerModule = defineWorkerModule({
name: 'FontProcessor',
dependencies: [
CONFIG,
Expand All @@ -218,7 +241,7 @@ export const fontProcessorWorkerModule = defineWorkerModule({
}
})

export const processInWorker = defineWorkerModule({
const processInWorker = defineWorkerModule({
name: 'TextBuilder',
dependencies: [fontProcessorWorkerModule, ThenableWorkerModule],
init(fontProcessor, Thenable) {
Expand Down Expand Up @@ -270,3 +293,11 @@ window._dumpSDFs = function() {
})
}
*/


export {
configureTextBuilder,
getTextRenderInfo,
preloadFont,
fontProcessorWorkerModule
}
6 changes: 5 additions & 1 deletion packages/troika-3d-text/src/index-textmesh-standalone.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// Exports for standalone TextMesh-only build:

export {configureTextBuilder, fontProcessorWorkerModule} from './TextBuilder.js'
export {
configureTextBuilder,
fontProcessorWorkerModule,
preloadFont
} from './TextBuilder.js'

export {TextMesh} from './three/TextMesh.js'
export {GlyphsGeometry} from './three/GlyphsGeometry.js'
6 changes: 5 additions & 1 deletion packages/troika-3d-text/src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// 3D text exports

export {configureTextBuilder, fontProcessorWorkerModule} from './TextBuilder.js'
export {
configureTextBuilder,
fontProcessorWorkerModule,
preloadFont
} from './TextBuilder.js'

export {TextMesh} from './three/TextMesh.js'
export {GlyphsGeometry} from './three/GlyphsGeometry.js'
Expand Down

0 comments on commit acedd3c

Please sign in to comment.