ntk 8.12.4 → 8.12.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/text/fontmanager.js +93 -43
- package/package.json +1 -1
package/lib/text/fontmanager.js
CHANGED
|
@@ -63,6 +63,60 @@ function shapingKeyOf(style) {
|
|
|
63
63
|
return `${feats}|${language ?? ''}|${letterSpacing || 0}`;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* How many shaped words a generation of the memo holds. Two are kept, so a
|
|
68
|
+
* working set up to twice this survives a relayout; what was asked for in
|
|
69
|
+
* neither generation is dropped when the current one fills.
|
|
70
|
+
*/
|
|
71
|
+
const SHAPE_GENERATION = 4000;
|
|
72
|
+
|
|
73
|
+
// The part of a shaping key a style decides, per style object. Built once a
|
|
74
|
+
// span rather than once a word: TextLayout asks for every word of a span with
|
|
75
|
+
// the same object, and the 2D context builds one object per `font` it is
|
|
76
|
+
// given. Neither changes an object after handing it over, so the only field
|
|
77
|
+
// checked again is the resolved face, the one a caller sets after building
|
|
78
|
+
// the rest.
|
|
79
|
+
const shapePrefixes = new WeakMap();
|
|
80
|
+
|
|
81
|
+
/** The key a style's words are memoized under, with the embedding levels
|
|
82
|
+
* they were shaped at: one string per style and level, kept, so a lookup
|
|
83
|
+
* hashes nothing it has not hashed before. */
|
|
84
|
+
function shapeGroupOf(style, levelsKey) {
|
|
85
|
+
const font = style.font;
|
|
86
|
+
let known = shapePrefixes.get(style);
|
|
87
|
+
if (known === undefined || known.font !== font) {
|
|
88
|
+
known = { font, prefix: shapePrefixOf(style), levels: new Map() };
|
|
89
|
+
shapePrefixes.set(style, known);
|
|
90
|
+
}
|
|
91
|
+
// a fragment's levels are one number in the common case, a list only
|
|
92
|
+
// for mixed-direction text; a list is not worth keeping
|
|
93
|
+
if (levelsKey.length > 2) return `${known.prefix}|${levelsKey}`;
|
|
94
|
+
let group = known.levels.get(levelsKey);
|
|
95
|
+
if (group === undefined) {
|
|
96
|
+
group = `${known.prefix}|${levelsKey}`;
|
|
97
|
+
known.levels.set(levelsKey, group);
|
|
98
|
+
}
|
|
99
|
+
return group;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function shapePrefixOf(style) {
|
|
103
|
+
const font = style.font;
|
|
104
|
+
// A resolved `font` already carries its coordinates in its key, so the
|
|
105
|
+
// variations and optical-size fragments only earn their keep on the
|
|
106
|
+
// family path — where two points of one axis would otherwise share a
|
|
107
|
+
// shaped run, and the second would be drawn with the first's advances.
|
|
108
|
+
// Features, language and spacing change the glyphs a word shapes to, so
|
|
109
|
+
// they are part of the key on both paths: without them a word shaped once
|
|
110
|
+
// plain answered every later request for it with the plain glyphs, and a
|
|
111
|
+
// `tnum` asked for after that point was silently ignored.
|
|
112
|
+
const shaping = shapingKeyOf(style);
|
|
113
|
+
return font
|
|
114
|
+
? `${font.key}|${style.size}|${style.weight}|${style.style}|${shaping}`
|
|
115
|
+
: `${style.family}|${style.size}|${style.weight}|${style.style}|${variationsKeyOf(
|
|
116
|
+
style.variations
|
|
117
|
+
)}|${opticalKeyOf(style)}|${shaping}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
66
120
|
/** Cache-key fragment for the optical size, `''` when the axis is left alone. */
|
|
67
121
|
function opticalKeyOf(style) {
|
|
68
122
|
return opticalSizeFor(style) ?? '';
|
|
@@ -136,7 +190,11 @@ export default class FontManager {
|
|
|
136
190
|
this._matches = new Map(); // family|weight|style -> Font
|
|
137
191
|
this._fallbacks = new Map(); // family|weight|style -> Map(codepoint -> Font|null)
|
|
138
192
|
this._registered = []; // { font, family (lowercase), weight, italic }
|
|
139
|
-
|
|
193
|
+
// word-level shaping memo, in two generations (`_shapeCached`): each a
|
|
194
|
+
// map from a style's key to the words shaped in it
|
|
195
|
+
this._shapeCache = new Map();
|
|
196
|
+
this._shapeCacheBefore = new Map();
|
|
197
|
+
this._shapeCount = 0;
|
|
140
198
|
}
|
|
141
199
|
|
|
142
200
|
/** the FontSource in effect (explicit, else the process-wide default) */
|
|
@@ -355,53 +413,45 @@ export default class FontManager {
|
|
|
355
413
|
}
|
|
356
414
|
|
|
357
415
|
/**
|
|
358
|
-
* Memoized shaping used by TextLayout and the canvas text path
|
|
359
|
-
*
|
|
360
|
-
* number when uniform for the whole
|
|
361
|
-
* comma-separated per-char levels.
|
|
416
|
+
* Memoized shaping used by TextLayout and the canvas text path, bounded at
|
|
417
|
+
* two generations of `SHAPE_GENERATION` words. `levelsKey` is a compact
|
|
418
|
+
* embedding-levels encoding: a single number when uniform for the whole
|
|
419
|
+
* fragment (the common case), else comma-separated per-char levels.
|
|
362
420
|
*/
|
|
363
421
|
_shapeCached(text, style, levelsKey = '0') {
|
|
364
|
-
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
this
|
|
384
|
-
this._shapeCache.set(key, shaped);
|
|
385
|
-
return shaped;
|
|
422
|
+
// A hit in this generation is the whole cost of a word a paragraph has
|
|
423
|
+
// shaped before — a relayout at a new width asks for every word again,
|
|
424
|
+
// a hundred thousand of them in a long document — so it is two lookups
|
|
425
|
+
// and nothing built: the style's key is the same string every time
|
|
426
|
+
// (`shapeGroupOf`), and the word is the only thing hashed. What the
|
|
427
|
+
// generation before holds is carried over when asked for; what neither
|
|
428
|
+
// was asked for goes when the generation turns.
|
|
429
|
+
const group = shapeGroupOf(style, levelsKey);
|
|
430
|
+
let words = this._shapeCache.get(group);
|
|
431
|
+
let shaped = words?.get(text);
|
|
432
|
+
if (shaped) return shaped;
|
|
433
|
+
shaped = this._shapeCacheBefore.get(group)?.get(text);
|
|
434
|
+
if (!shaped) {
|
|
435
|
+
let levels;
|
|
436
|
+
if (levelsKey.includes(',')) {
|
|
437
|
+
levels = levelsKey.split(',').map(Number);
|
|
438
|
+
} else {
|
|
439
|
+
levels = new Uint8Array(text.length).fill(Number(levelsKey));
|
|
440
|
+
}
|
|
441
|
+
shaped = shapeText(this, text, style, levels);
|
|
386
442
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
443
|
+
if (this._shapeCount >= SHAPE_GENERATION) {
|
|
444
|
+
this._shapeCacheBefore = this._shapeCache;
|
|
445
|
+
this._shapeCache = new Map();
|
|
446
|
+
this._shapeCount = 0;
|
|
447
|
+
words = undefined;
|
|
392
448
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
// a live UI's working set sits at the recent end and survives, where
|
|
397
|
-
// the old wholesale clear() re-shaped everything on screen
|
|
398
|
-
let drop = this._shapeCache.size >> 1;
|
|
399
|
-
for (const k of this._shapeCache.keys()) {
|
|
400
|
-
if (drop-- <= 0) break;
|
|
401
|
-
this._shapeCache.delete(k);
|
|
402
|
-
}
|
|
449
|
+
if (!words) {
|
|
450
|
+
words = new Map();
|
|
451
|
+
this._shapeCache.set(group, words);
|
|
403
452
|
}
|
|
404
|
-
|
|
453
|
+
words.set(text, shaped);
|
|
454
|
+
this._shapeCount++;
|
|
405
455
|
return shaped;
|
|
406
456
|
}
|
|
407
457
|
|