1
- import { Color , DataTexture , LinearFilter , RGBAFormat } from 'three'
2
- import { defineWorkerModule , terminateWorker , ThenableWorkerModule , Thenable } from 'troika-worker-utils'
3
- import { createSDFGenerator } from './worker/SDFGenerator.js'
1
+ import { Color , CanvasTexture , DataTexture , LinearFilter } from 'three'
2
+ import { defineWorkerModule , ThenableWorkerModule , Thenable } from 'troika-worker-utils'
4
3
import { createTypesetter } from './worker/Typesetter.js'
5
- import { createGlyphSegmentsIndex } from './worker/GlyphSegmentsIndex.js'
4
+ import { generateSDF , warmUpSDFCanvas , resizeWebGLCanvasWithoutClearing } from './worker/SDFGenerator.js'
5
+
6
6
import bidiFactory from 'bidi-js'
7
7
8
8
// Choose parser impl:
@@ -150,16 +150,16 @@ function getTextRenderInfo(args, callback) {
150
150
// Init the atlas if needed
151
151
const { textureWidth, sdfExponent} = CONFIG
152
152
const { sdfGlyphSize} = args
153
+ const glyphsPerRow = ( textureWidth / sdfGlyphSize * 4 )
153
154
let atlas = atlases [ sdfGlyphSize ]
154
155
if ( ! atlas ) {
156
+ const canvas = document . createElement ( 'canvas' )
157
+ canvas . width = textureWidth
158
+ canvas . height = sdfGlyphSize * 256 / glyphsPerRow // start tall enough to fit 256 glyphs
155
159
atlas = atlases [ sdfGlyphSize ] = {
156
160
glyphCount : 0 ,
157
- sdfTexture : new DataTexture (
158
- new Uint8Array ( sdfGlyphSize * textureWidth * 4 ) ,
159
- textureWidth ,
160
- sdfGlyphSize ,
161
- RGBAFormat ,
162
- undefined ,
161
+ sdfTexture : new CanvasTexture (
162
+ canvas ,
163
163
undefined ,
164
164
undefined ,
165
165
undefined ,
@@ -170,6 +170,7 @@ function getTextRenderInfo(args, callback) {
170
170
}
171
171
}
172
172
173
+ const { sdfTexture} = atlas
173
174
let fontGlyphs = atlas . glyphsByFont . get ( args . font )
174
175
if ( ! fontGlyphs ) {
175
176
atlas . glyphsByFont . set ( args . font , fontGlyphs = new Map ( ) )
@@ -228,52 +229,37 @@ function getTextRenderInfo(args, callback) {
228
229
const sdfStart = now ( )
229
230
timings . sdf = { }
230
231
232
+ // Grow the texture height by power of 2 if needed
233
+ const currentHeight = sdfTexture . image . height
234
+ const neededRows = Math . ceil ( atlas . glyphCount / glyphsPerRow )
235
+ const neededHeight = Math . pow ( 2 , Math . ceil ( Math . log2 ( neededRows * sdfGlyphSize ) ) )
236
+ if ( neededHeight > currentHeight ) {
237
+ // Since resizing the canvas clears its render buffer, it needs special handling to copy the old contents over
238
+ console . info ( `Increasing SDF texture size ${ currentHeight } ->${ neededHeight } ` )
239
+ resizeWebGLCanvasWithoutClearing ( sdfTexture . image , textureWidth , neededHeight )
240
+ }
241
+
231
242
Thenable . all ( neededSDFs . map ( ( { path, atlasIndex, sdfViewBox} ) => {
232
243
const maxDist = Math . max ( sdfViewBox [ 2 ] - sdfViewBox [ 0 ] , sdfViewBox [ 3 ] - sdfViewBox [ 1 ] )
233
- return generateSDFInWorker ( sdfGlyphSize , sdfGlyphSize , path , sdfViewBox , maxDist , CONFIG . sdfExponent )
234
- . then ( ( { textureData, timing} ) => {
244
+ const squareIndex = Math . floor ( atlasIndex / 4 )
245
+ const x = squareIndex % ( textureWidth / sdfGlyphSize ) * sdfGlyphSize
246
+ const y = Math . floor ( squareIndex / ( textureWidth / sdfGlyphSize ) ) * sdfGlyphSize
247
+ const channel = atlasIndex % 4
248
+ return generateSDF ( sdfGlyphSize , sdfGlyphSize , path , sdfViewBox , maxDist , CONFIG . sdfExponent , sdfTexture . image , x , y , channel )
249
+ . then ( ( { timing} ) => {
235
250
timings . sdf [ atlasIndex ] = timing
236
- return { atlasIndex, textureData, timing }
237
- } )
238
- } ) ) . then ( sdfResults => {
239
- // If we have new SDFs, copy them into the atlas texture at the specified indices
240
- if ( sdfResults . length ) {
241
- sdfResults . forEach ( ( { atlasIndex, textureData } ) => {
242
- const texImg = atlas . sdfTexture . image
243
-
244
- // Grow the texture by power of 2 if needed
245
- while ( texImg . data . length < ( atlasIndex + 1 ) * sdfGlyphSize * sdfGlyphSize ) {
246
- const biggerArray = new Uint8Array ( texImg . data . length * 2 )
247
- biggerArray . set ( texImg . data )
248
- texImg . data = biggerArray
249
- texImg . height *= 2
250
- }
251
-
252
- // Insert the new glyph's data into the full texture image at the correct offsets
253
- // Glyphs are packed sequentially into the R,G,B,A channels of a square, advancing
254
- // to the next square every 4 glyphs.
255
- const squareIndex = Math . floor ( atlasIndex / 4 )
256
- const cols = texImg . width / sdfGlyphSize
257
- const baseStartIndex = Math . floor ( squareIndex / cols ) * texImg . width * sdfGlyphSize * 4 //full rows
258
- + ( squareIndex % cols ) * sdfGlyphSize * 4 //partial row
259
- + ( atlasIndex % 4 ) //color channel
260
- for ( let y = 0 ; y < sdfGlyphSize ; y ++ ) {
261
- const srcStartIndex = y * sdfGlyphSize
262
- const rowStartIndex = baseStartIndex + ( y * texImg . width * 4 )
263
- for ( let x = 0 ; x < sdfGlyphSize ; x ++ ) {
264
- texImg . data [ rowStartIndex + x * 4 ] = textureData [ srcStartIndex + x ]
265
- }
266
- }
267
251
} )
268
- atlas . sdfTexture . needsUpdate = true
269
- }
252
+ } ) ) . then ( ( ) => {
270
253
timings . sdfTotal = now ( ) - sdfStart
271
254
timings . total = now ( ) - totalStart
255
+ if ( neededSDFs . length ) {
256
+ sdfTexture . needsUpdate = true
257
+ }
272
258
273
259
// Invoke callback with the text layout arrays and updated texture
274
260
callback ( Object . freeze ( {
275
261
parameters : args ,
276
- sdfTexture : atlas . sdfTexture ,
262
+ sdfTexture,
277
263
sdfGlyphSize,
278
264
sdfExponent,
279
265
glyphBounds,
@@ -301,6 +287,11 @@ function getTextRenderInfo(args, callback) {
301
287
} ) )
302
288
} )
303
289
} )
290
+
291
+ // While the typesetting request is being handled, go ahead and make sure the atlas canvas context is
292
+ // "warmed up"; the first request will be the longest due to shader program compilation so this gets
293
+ // a head start on that process before SDFs actually start getting processed.
294
+ Thenable . all ( [ ] ) . then ( ( ) => warmUpSDFCanvas ( sdfTexture . image ) )
304
295
}
305
296
306
297
@@ -361,53 +352,6 @@ const typesetterWorkerModule = /*#__PURE__*/defineWorkerModule({
361
352
}
362
353
} )
363
354
364
- /**
365
- * SDF generator function wrapper that fans out requests to a number of worker
366
- * threads for parallelism
367
- */
368
- const generateSDFInWorker = /*#__PURE__*/ function ( ) {
369
- const threadCount = 4 //how many workers to spawn
370
- const idleTimeout = 2000 //workers will be terminated after being idle this many milliseconds
371
- const threads = { }
372
- let callNum = 0
373
- return function ( ...args ) {
374
- const workerId = 'TroikaTextSDFGenerator_' + ( ( callNum ++ ) % threadCount )
375
- let thread = threads [ workerId ]
376
- if ( ! thread ) {
377
- thread = threads [ workerId ] = {
378
- workerModule : defineWorkerModule ( {
379
- name : workerId ,
380
- workerId,
381
- dependencies : [
382
- CONFIG ,
383
- createGlyphSegmentsIndex ,
384
- createSDFGenerator
385
- ] ,
386
- init ( config , createGlyphSegmentsIndex , createSDFGenerator ) {
387
- const { sdfExponent, sdfMargin} = config
388
- return createSDFGenerator ( createGlyphSegmentsIndex , { sdfExponent, sdfMargin } )
389
- } ,
390
- getTransferables ( result ) {
391
- return [ result . textureData . buffer ]
392
- }
393
- } ) ,
394
- requests : 0 ,
395
- idleTimer : null
396
- }
397
- }
398
-
399
- thread . requests ++
400
- clearTimeout ( thread . idleTimer )
401
- return thread . workerModule ( ...args )
402
- . then ( result => {
403
- if ( -- thread . requests === 0 ) {
404
- thread . idleTimer = setTimeout ( ( ) => { terminateWorker ( workerId ) } , idleTimeout )
405
- }
406
- return result
407
- } )
408
- }
409
- } ( )
410
-
411
355
const typesetInWorker = /*#__PURE__*/ defineWorkerModule ( {
412
356
name : 'Typesetter' ,
413
357
dependencies : [
@@ -438,16 +382,9 @@ const typesetInWorker = /*#__PURE__*/defineWorkerModule({
438
382
} )
439
383
440
384
function dumpSDFTextures ( ) {
441
- Object . keys ( atlases ) . forEach ( font => {
442
- const atlas = atlases [ font ]
443
- const canvas = document . createElement ( 'canvas' )
444
- const { width, height, data} = atlas . sdfTexture . image
445
- canvas . width = width
446
- canvas . height = height
447
- const imgData = new ImageData ( new Uint8ClampedArray ( data ) , width , height )
448
- const ctx = canvas . getContext ( '2d' )
449
- ctx . putImageData ( imgData , 0 , 0 )
450
- console . log ( font , atlas , canvas . toDataURL ( ) )
385
+ Object . keys ( atlases ) . forEach ( size => {
386
+ const canvas = atlases [ size ] . sdfTexture . image
387
+ const { width, height} = canvas
451
388
console . log ( "%c." , `
452
389
background: url(${ canvas . toDataURL ( ) } );
453
390
background-size: ${ width } px ${ height } px;
0 commit comments