ntk 6.0.0 → 6.0.1
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/app.js +46 -0
- package/lib/fontconfig.js +70 -13
- package/lib/index.js +36 -18
- package/lib/renderingcontext_2d.js +901 -266
- package/lib/text/fontmanager.js +52 -14
- package/lib/text/fontsource.js +12 -1
- package/lib/text/layout.js +1 -14
- package/lib/text/shape.js +17 -0
- package/lib/window.js +18 -3
- package/package.json +1 -1
package/lib/text/fontmanager.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
detectStyle,
|
|
6
6
|
numericWeight as numWeight
|
|
7
7
|
} from './fontsource.js';
|
|
8
|
-
import { shapeText } from './shape.js';
|
|
8
|
+
import { embeddingLevels, normalizedLevels, shapeText } from './shape.js';
|
|
9
9
|
import { TextLayout } from './layout.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -38,7 +38,7 @@ export default class FontManager {
|
|
|
38
38
|
this._matches = new Map(); // family|weight|style -> Font
|
|
39
39
|
this._fallbacks = new Map(); // family|weight|style -> Map(codepoint -> Font|null)
|
|
40
40
|
this._registered = []; // { font, family (lowercase), weight, italic }
|
|
41
|
-
this._shapeCache = new Map(); // word-level shaping memo (bounded)
|
|
41
|
+
this._shapeCache = new Map(); // word-level shaping memo (bounded, LRU)
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/** the FontSource in effect (explicit, else the process-wide default) */
|
|
@@ -232,25 +232,63 @@ export default class FontManager {
|
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
/**
|
|
235
|
-
* Memoized shaping used by TextLayout
|
|
236
|
-
* compact embedding-levels encoding: a single
|
|
237
|
-
* whole fragment (the common case), else
|
|
235
|
+
* Memoized shaping used by TextLayout and the canvas text path (bounded
|
|
236
|
+
* LRU). `levelsKey` is a compact embedding-levels encoding: a single
|
|
237
|
+
* number when uniform for the whole fragment (the common case), else
|
|
238
|
+
* comma-separated per-char levels.
|
|
238
239
|
*/
|
|
239
240
|
_shapeCached(text, style, levelsKey = '0') {
|
|
240
241
|
const font = style.font;
|
|
241
242
|
const key = `${font ? font.key : style.family}|${style.size}|${style.weight}|${style.style}|${levelsKey}|${text}`;
|
|
242
243
|
let shaped = this._shapeCache.get(key);
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
} else {
|
|
248
|
-
levels = new Uint8Array(text.length).fill(Number(levelsKey));
|
|
249
|
-
}
|
|
250
|
-
shaped = shapeText(this, text, style, levels);
|
|
251
|
-
if (this._shapeCache.size > 4000) this._shapeCache.clear();
|
|
244
|
+
if (shaped) {
|
|
245
|
+
// Map iterates in insertion order: re-inserting a hit moves it to the
|
|
246
|
+
// tail, so the eviction sweep below walks least-recently-used first
|
|
247
|
+
this._shapeCache.delete(key);
|
|
252
248
|
this._shapeCache.set(key, shaped);
|
|
249
|
+
return shaped;
|
|
250
|
+
}
|
|
251
|
+
let levels;
|
|
252
|
+
if (levelsKey.includes(',')) {
|
|
253
|
+
levels = levelsKey.split(',').map(Number);
|
|
254
|
+
} else {
|
|
255
|
+
levels = new Uint8Array(text.length).fill(Number(levelsKey));
|
|
253
256
|
}
|
|
257
|
+
shaped = shapeText(this, text, style, levels);
|
|
258
|
+
if (this._shapeCache.size > 4000) {
|
|
259
|
+
// drop the stale half in one sweep rather than one entry per insert —
|
|
260
|
+
// a live UI's working set sits at the recent end and survives, where
|
|
261
|
+
// the old wholesale clear() re-shaped everything on screen
|
|
262
|
+
let drop = this._shapeCache.size >> 1;
|
|
263
|
+
for (const k of this._shapeCache.keys()) {
|
|
264
|
+
if (drop-- <= 0) break;
|
|
265
|
+
this._shapeCache.delete(k);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
this._shapeCache.set(key, shaped);
|
|
254
269
|
return shaped;
|
|
255
270
|
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Memoized whole-string shaping — what `ctx.fillText` draws through, so
|
|
274
|
+
* repainting the same label hits the same entries TextLayout populates
|
|
275
|
+
* instead of re-running bidi + OpenType shaping every frame.
|
|
276
|
+
*
|
|
277
|
+
* Bidi is resolved here because the memo key needs the embedding levels;
|
|
278
|
+
* the paragraph level rides back on the result separately, since per-char
|
|
279
|
+
* levels cannot recover it (RTL text in an RTL paragraph carries level 1,
|
|
280
|
+
* which reads as an even base) and start/end alignment depends on it.
|
|
281
|
+
*
|
|
282
|
+
* `features` and `language` change shaped output but are not part of the
|
|
283
|
+
* memo key (styles reaching it via TextLayout never carry them into the
|
|
284
|
+
* key either), so a style with either shapes uncached rather than
|
|
285
|
+
* poisoning entries other callers share.
|
|
286
|
+
*/
|
|
287
|
+
_shapeCachedWhole(text, style) {
|
|
288
|
+
if (style.features || style.language) return shapeText(this, text, style);
|
|
289
|
+
const emb = embeddingLevels(text, style.direction);
|
|
290
|
+
const baseLevel = emb.paragraphs.length ? emb.paragraphs[0].level : 0;
|
|
291
|
+
const shaped = this._shapeCached(text, style, normalizedLevels(emb.levels, 0, text.length));
|
|
292
|
+
return shaped.baseLevel === baseLevel ? shaped : { ...shaped, baseLevel };
|
|
293
|
+
}
|
|
256
294
|
}
|
package/lib/text/fontsource.js
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
// than imported, because this file is the one the browser bundle keeps — its
|
|
35
35
|
// whole purpose is running the text stack without a filesystem. Do not turn
|
|
36
36
|
// that into a static import (test/packaging.test.js fails if you do).
|
|
37
|
-
import { charsetHas, matchSortedSync, noFontsError, supported } from '../fontconfig.js';
|
|
37
|
+
import { charsetHas, matchSortedSync, noFontsError, prewarm, supported } from '../fontconfig.js';
|
|
38
38
|
import Font from './font.js';
|
|
39
39
|
|
|
40
40
|
const WEIGHTS = { normal: 400, bold: 700 };
|
|
@@ -95,6 +95,17 @@ export function parseFamilies(family) {
|
|
|
95
95
|
* self-contained alternative.
|
|
96
96
|
*/
|
|
97
97
|
export class FontconfigFontSource {
|
|
98
|
+
constructor() {
|
|
99
|
+
// Construction is the moment fontconfig is chosen as the lookup path, so
|
|
100
|
+
// the fc-match spawn for the pattern every widget default resolves to —
|
|
101
|
+
// sans-serif at regular weight, exactly as FontManager.match normalizes
|
|
102
|
+
// it — starts here, asynchronously, instead of stalling the first text
|
|
103
|
+
// layout for ~50ms. Fire-and-forget: prewarm never rejects, and a source
|
|
104
|
+
// constructed where fc-match is missing stays silent until (unless) text
|
|
105
|
+
// is actually rendered.
|
|
106
|
+
prewarm({ family: 'sans-serif', weight: 400, style: 'normal' });
|
|
107
|
+
}
|
|
108
|
+
|
|
98
109
|
matchSorted(pattern) {
|
|
99
110
|
return matchSortedSync(pattern);
|
|
100
111
|
}
|
package/lib/text/layout.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import LineBreaker from 'linebreak';
|
|
2
2
|
|
|
3
3
|
import { drawGlyphRuns } from './glyphs.js';
|
|
4
|
-
import { embeddingLevels, reorderRuns } from './shape.js';
|
|
4
|
+
import { embeddingLevels, normalizedLevels, reorderRuns } from './shape.js';
|
|
5
5
|
|
|
6
6
|
const WS = new Set([0x20, 0x09, 0xa0]);
|
|
7
7
|
const HARD_BREAKS = /[\n\r\u2028\u2029]+$/;
|
|
@@ -856,16 +856,3 @@ function sliceLevels(key, start, end) {
|
|
|
856
856
|
if (!key.includes(',')) return key;
|
|
857
857
|
return key.split(',').slice(start, end).join(',');
|
|
858
858
|
}
|
|
859
|
-
|
|
860
|
-
// compact levels key for the shaping cache: single char when uniform
|
|
861
|
-
function normalizedLevels(levels, start, end) {
|
|
862
|
-
let uniform = true;
|
|
863
|
-
for (let i = start + 1; i < end; i++) {
|
|
864
|
-
if (levels[i] !== levels[start]) {
|
|
865
|
-
uniform = false;
|
|
866
|
-
break;
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
if (uniform) return String(levels[start] ?? 0);
|
|
870
|
-
return Array.prototype.slice.call(levels, start, end).join(',');
|
|
871
|
-
}
|
package/lib/text/shape.js
CHANGED
|
@@ -28,6 +28,23 @@ export function embeddingLevels(text, direction = 'auto') {
|
|
|
28
28
|
return bidi.getEmbeddingLevels(text, explicit);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Compact embedding-levels encoding for the shaping memo key: a single
|
|
33
|
+
* number when uniform over [start, end) — the common case — else
|
|
34
|
+
* comma-separated per-char levels.
|
|
35
|
+
*/
|
|
36
|
+
export function normalizedLevels(levels, start, end) {
|
|
37
|
+
let uniform = true;
|
|
38
|
+
for (let i = start + 1; i < end; i++) {
|
|
39
|
+
if (levels[i] !== levels[start]) {
|
|
40
|
+
uniform = false;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (uniform) return String(levels[start] ?? 0);
|
|
45
|
+
return Array.prototype.slice.call(levels, start, end).join(',');
|
|
46
|
+
}
|
|
47
|
+
|
|
31
48
|
/**
|
|
32
49
|
* Shape a string through the full pipeline:
|
|
33
50
|
*
|
package/lib/window.js
CHANGED
|
@@ -27,6 +27,19 @@ const MAX_DIRTY_RECTS = 8;
|
|
|
27
27
|
*/
|
|
28
28
|
const SPLIT_SAVING = 0.75;
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Backing pixmap dimensions are rounded up to this granularity.
|
|
32
|
+
*
|
|
33
|
+
* An interactive enlarge delivers a resize per frame, and an exact grow-only
|
|
34
|
+
* allocation crosses its previous maximum on every one of them — each frame
|
|
35
|
+
* paying for a CreatePixmap, a full white clear, a FreePixmap and a picture
|
|
36
|
+
* rebind. With headroom the reuse check absorbs the intermediate steps and a
|
|
37
|
+
* continuous drag reallocates O(log) times. The spare pixels are never
|
|
38
|
+
* presented — every blit is clamped to window ∩ backing — they just sit
|
|
39
|
+
* white until the window grows into them.
|
|
40
|
+
*/
|
|
41
|
+
const BACKING_GRANULARITY = 128;
|
|
42
|
+
|
|
30
43
|
function rectArea(r) {
|
|
31
44
|
return Math.max(0, r.w) * Math.max(0, r.h);
|
|
32
45
|
}
|
|
@@ -639,12 +652,14 @@ export default class Window extends Drawable {
|
|
|
639
652
|
});
|
|
640
653
|
}
|
|
641
654
|
|
|
642
|
-
// grow-only backing pixmap (re)allocation
|
|
655
|
+
// grow-only backing pixmap (re)allocation, with headroom (see
|
|
656
|
+
// BACKING_GRANULARITY); new area is cleared to white
|
|
643
657
|
_allocBacking(w, h) {
|
|
644
658
|
const cur = this._backing;
|
|
645
659
|
if (cur && cur.width >= w && cur.height >= h) return;
|
|
646
|
-
const
|
|
647
|
-
const
|
|
660
|
+
const roundUp = (v) => Math.ceil(v / BACKING_GRANULARITY) * BACKING_GRANULARITY;
|
|
661
|
+
const newW = roundUp(Math.max(w, cur ? cur.width : 0));
|
|
662
|
+
const newH = roundUp(Math.max(h, cur ? cur.height : 0));
|
|
648
663
|
const pixmap = new Pixmap(this.app, {
|
|
649
664
|
parent: this,
|
|
650
665
|
width: newW,
|