@weasel-js/font 1.9.4 → 1.10.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.
@@ -29,8 +29,43 @@ interface GlyphTextureSink {
29
29
  upload(id: string, source: TexSource): string;
30
30
  uploadR8(id: string, width: number, height: number, data: Uint8Array): void;
31
31
  subImageR8(id: string, x: number, y: number, w: number, h: number, data: Uint8Array): void;
32
+ /** Create a texture of premultiplied RGBA texels that are a color, not a
33
+ * field, so it may be minified through mipmaps. */
34
+ uploadRGBA(id: string, width: number, height: number, data: Uint8Array): void;
35
+ subImageRGBA(id: string, x: number, y: number, w: number, h: number, data: Uint8Array): void;
32
36
  }
33
37
 
38
+ /**
39
+ * The dynamic glyph atlas's pages: their CPU-side bytes, and bringing each
40
+ * renderer's texture of one up to date. Two sets: distance fields, one byte a
41
+ * texel, and color glyphs, four.
42
+ */
43
+
44
+ interface DynamicPage {
45
+ data: Uint8Array;
46
+ /** Bumps on every write; each write appends a patch with seq = version. */
47
+ version: number;
48
+ patches: {
49
+ seq: number;
50
+ x: number;
51
+ y: number;
52
+ w: number;
53
+ h: number;
54
+ }[];
55
+ }
56
+ /** Texture-cache key for a dynamic field page (parallel to `textureCacheKey`). */
57
+ declare function dynamicPageTextureId(page: number): string;
58
+ /** Texture-cache key for a page of color glyphs. */
59
+ declare function colorPageTextureId(page: number): string;
60
+ /** Bring `cache`'s copy of field page `pageIndex` up to date: full R8 upload
61
+ * the first time, `texSubImage2D` patches after. Returns false if the page
62
+ * doesn't exist yet. */
63
+ declare function syncDynamicPageTexture(cache: GlyphTextureSink, pageIndex: number): boolean;
64
+ /** The same for a page of color glyphs, whose texels are premultiplied RGBA. */
65
+ declare function syncColorPageTexture(cache: GlyphTextureSink, pageIndex: number): boolean;
66
+ /** @internal test seam — inspect CPU-side field pages. */
67
+ declare function _getPagesForTests(): readonly DynamicPage[];
68
+
34
69
  /**
35
70
  * Canvas-2D glyph rasterizer for the dynamic SDF atlas. `fillText` is the
36
71
  * only web-platform route to *installed* machine fonts (no outline access),
@@ -43,28 +78,18 @@ interface GlyphTextureSink {
43
78
  * The bitmap includes a PAD border on all four sides so the SDF field has
44
79
  * room to fall off. Blank glyphs (e.g. space) return width/height 0.
45
80
  *
46
- * On BAKE_SIZE — measured 2026-07-29, comparing the reconstructed field
47
- * against a direct `fillText` of the same glyph at each display size, with
48
- * sub-pixel registration searched out so it measures fidelity and not
49
- * alignment. Mean per-pixel coverage error, averaged over A/S/e/W/8:
81
+ * On BAKE_SIZE: mean per-pixel coverage error of the reconstructed field
82
+ * against a direct `fillText` of the same glyph at each display size:
50
83
  *
51
84
  * display px 12 16 24 32 48 64 96 128
52
85
  * error .096 .083 .059 .039 .024 .023 .037 .049
53
86
  *
54
- * Two things follow, both of them counterintuitive enough to be worth
55
- * writing down. The error is minimized *at* the bake size, so raising
56
- * BAKE_SIZE does not improve small text — it moves the sweet spot away from
57
- * the 12–32px range where UI text actually lives and makes it worse. And the
58
- * magnification side is the mild one: 128px costs about twice the floor,
59
- * which is the corner rounding the shader header already owns.
60
- *
61
- * The 12–16px end is the largest divergence, and it is not undersampling —
62
- * 3×3 supersampling the reconstruction roughly halves the error at 24–48px
63
- * but recovers almost nothing at 12–16px (.096 → .089, .083 → .068). What is
64
- * left there is a hinted rasterizer placing stems on the pixel grid, which no
65
- * size-independent field can encode. So there is nothing to buy with more
66
- * taps, a bigger bake, or mipmaps (which a packed atlas cannot have anyway —
67
- * mip levels blend across glyph rects).
87
+ * The error is minimized *at* the bake size, so raising BAKE_SIZE does not
88
+ * improve small text — it moves the sweet spot away from the 12–32px range
89
+ * and makes it worse. The 12–16px error is not undersampling: supersampling
90
+ * the reconstruction recovers almost nothing there. What is left is a hinted
91
+ * rasterizer placing stems on the pixel grid, which no size-independent field
92
+ * can encode, so more taps, a bigger bake, or mipmaps buy nothing.
68
93
  */
69
94
 
70
95
  interface FaceMetrics$1 {
@@ -79,15 +104,29 @@ interface RasterizedGlyph {
79
104
  top: number;
80
105
  advance: number;
81
106
  }
107
+ /** A glyph in its own colors. Placed as a {@link RasterizedGlyph} is. */
108
+ interface RasterizedColorGlyph {
109
+ width: number;
110
+ height: number;
111
+ /** Premultiplied RGBA, four bytes a texel. */
112
+ rgba: Uint8ClampedArray;
113
+ left: number;
114
+ top: number;
115
+ advance: number;
116
+ }
82
117
  interface GlyphRasterizer {
83
118
  faceMetrics(family: string, weight: number, style: FontStyle): FaceMetrics$1;
84
119
  rasterize(family: string, weight: number, style: FontStyle, codepoint: number): RasterizedGlyph;
120
+ /** `text`, one grapheme cluster, as the color glyph the platform draws for
121
+ * it, or null when it draws none: no ink, or ink in the fill color alone. */
122
+ rasterizeColor?(family: string, weight: number, style: FontStyle, text: string): RasterizedColorGlyph | null;
85
123
  }
86
124
 
87
125
  /**
88
126
  * DynamicGlyphAtlas — runtime single-channel SDF glyphs for canvas-sourced
89
127
  * (installed machine) fonts. TinySDF technique: canvas fillText at 48 px →
90
- * Euclidean distance transform → shelf-packed R8 pages (1024², max 4).
128
+ * Euclidean distance transform → shelf-packed R8 pages (1024², max 4). When
129
+ * the pages fill they are emptied whole and the text on screen bakes again.
91
130
  *
92
131
  * Baked MSDF atlases always win: this tier only serves families registered
93
132
  * via `registerCanvasFont` that have no baked entry (see resolveFontVariant).
@@ -97,13 +136,13 @@ interface GlyphRasterizer {
97
136
  * baked atlases. A char's advance is valid immediately (measureText); its
98
137
  * atlas rect fills in when the bake lands (width 0 / page -1 until then, so
99
138
  * layout advances the pen but emits no quad).
139
+ *
140
+ * An emoji is the one glyph kept as the canvas drew it: `requestCluster`
141
+ * rasterizes a whole grapheme cluster in its own colors onto a second set of
142
+ * pages, RGBA, that fill and empty with the field pages.
100
143
  */
101
144
 
102
- /** How many glyphs the dynamic atlas will rasterize synchronously before
103
- * deferring the rest to a later flush. Caps the frame cost of text that
104
- * suddenly needs many new glyphs; the leftovers arrive a tick later and
105
- * notify through `onGlyphReady`. */
106
- declare const DEFAULT_BAKE_BUDGET = 16;
145
+ /** A face whose glyphs are rasterized on demand into a shared atlas page, rather than shipped as a prebuilt one. */
107
146
  interface DynamicFace {
108
147
  family: string;
109
148
  weight: number;
@@ -113,18 +152,13 @@ interface DynamicFace {
113
152
  /** Char record for `cp`, measured on first request (advance always valid
114
153
  * immediately; atlas rect fills in when the bake lands). */
115
154
  requestGlyph(cp: number): BmFontChar;
116
- }
117
- interface DynamicPage {
118
- data: Uint8Array;
119
- /** Bumps on every glyph blit; each blit appends a patch with seq = version. */
120
- version: number;
121
- patches: {
122
- seq: number;
123
- x: number;
124
- y: number;
125
- w: number;
126
- h: number;
127
- }[];
155
+ /**
156
+ * The color glyph the platform draws for the grapheme cluster `text`,
157
+ * measured on first request as `requestGlyph`'s is, or null when it draws
158
+ * none. Its record is marked `color` and its rect is on a color page. It is
159
+ * not in `font.charMap`, which holds one code point each.
160
+ */
161
+ requestCluster(text: string): BmFontChar | null;
128
162
  }
129
163
  /** One family served by the dynamic canvas-SDF tier. See `listCanvasFonts`. */
130
164
  interface CanvasFontEntry {
@@ -149,75 +183,49 @@ declare function registerCanvasFont(family: string): void;
149
183
  * so returning to `'canvas'` makes it `true` again.
150
184
  * - never enrolled → `false`, including under `'canvas'` (that policy
151
185
  * enrolls lazily, on the first miss).
152
- *
153
- * The membership reading would answer `true` for an auto-enrolled family
154
- * under `'substitute'` / `'none'`, where nothing routes to this tier — a
155
- * caller predicting what renders would be told the opposite of the truth.
156
- * Mirrors `listFonts`, which reports the baked registry alone for the same
157
- * reason: "enrolled" is not "will render".
158
186
  */
159
187
  declare function isCanvasFont(family: string): boolean;
160
188
  /**
161
189
  * Every family the dynamic canvas-SDF tier will serve *right now* — the
162
190
  * enumeration companion to `isCanvasFont`, which answers one family at a
163
191
  * time. Same "service, not membership" rule: an auto-enrolled family appears
164
- * only while the `'canvas'` policy is in force.
165
- *
166
- * Exists so a consumer can offer these families in a font picker.
167
- * `listFonts` reports the baked registry alone, so without this the only
168
- * ways to present canvas-served families were to hard-code a list beside the
169
- * `registerCanvasFont` calls or to omit them — a UI inventing its own answer
170
- * about what will render.
192
+ * only while the `'canvas'` policy is in force. `listFonts` reports the baked
193
+ * registry alone.
171
194
  *
172
195
  * `enrollment` distinguishes a consumer's explicit `registerCanvasFont` from
173
196
  * a lazy enrollment the policy made on a miss; both render identically, but
174
197
  * only the explicit one survives a policy change.
175
198
  */
176
199
  declare function listCanvasFonts(): readonly CanvasFontEntry[];
177
- /** Remove a canvas family. Its faces are dropped; already-baked glyph
178
- * pixels stay in their pages (no eviction in v1). */
200
+ /** Remove a canvas family. Its faces are dropped; the room its glyphs took
201
+ * comes back when the pages next fill and are emptied. */
179
202
  declare function unregisterCanvasFont(family: string): void;
180
- /** Reset the per-frame synchronous bake budget. Called by
181
- * `WeaselRenderer.render()` at frame start; the headless
182
- * `renderSceneToPixels` path passes Infinity so print never defers. */
183
- declare function resetBakeBudget(n?: number): void;
184
- /** Texture-cache key for a dynamic page (parallel to `textureCacheKey`). */
185
- declare function dynamicPageTextureId(page: number): string;
186
- /** Bring `cache`'s copy of page `pageIndex` up to date: full R8 upload the
187
- * first time, `texSubImage2D` patches after. Returns false if the page
188
- * doesn't exist yet. */
189
- declare function syncDynamicPageTexture(cache: GlyphTextureSink, pageIndex: number): boolean;
203
+ /**
204
+ * Measure `family`'s canvas faces again, or every family's when none is named.
205
+ *
206
+ * The tier keeps each face's metrics and each glyph's shape and advance from
207
+ * the first time it is asked. A web font that finishes loading after that is
208
+ * picked up from `document.fonts` without this call; a face the document
209
+ * never announces, one loaded before it was added, needs it.
210
+ */
211
+ declare function refreshCanvasFont(family?: string): void;
190
212
  /** @internal test seam — inject a fake rasterizer (jsdom has no canvas
191
213
  * metrics). Pass null to restore the lazy default. */
192
214
  declare function __setGlyphRasterizerForTests(r: GlyphRasterizer | null): void;
193
- /** @internal test seam — inspect CPU-side pages. */
194
- declare function _getPagesForTests(): readonly DynamicPage[];
195
215
  /** @internal test seam — clear all dynamic-font state. */
196
216
  declare function _resetDynamicFontsForTests(): void;
197
217
 
198
218
  /**
199
219
  * The parsed-font seam for the outline tier.
200
220
  *
201
- * A face answers exactly one question — "what does codepoint N look like?" —
202
- * and answers it as **SVG path data in em space**: one unit is one em, y
203
- * grows downward, and the origin sits on the baseline at the glyph's pen
204
- * position. That is a complete description of the glyph independent of size,
205
- * zoom, and position, which is the whole reason the tier exists: tessellate
206
- * once, transform per instance.
207
- *
208
- * ### Why `d` and not a command stream
209
- *
210
- * `@weasel-js/font` is a Tier A leaf — `@weasel-js/core` depends on it and
211
- * never the reverse (see `leaf-purity.test.ts`), so this package cannot name
212
- * core's `PolygonPath`. The alternative to a string would be re-declaring
213
- * core's `PATH_M`/`PATH_L`/… opcodes over here and trusting two packages to
214
- * keep the same numbering forever. SVG `d` is the kit's documented language
215
- * for geometry crossing a boundary (`docs/conventions.md`), core already
216
- * ships `pathFromD`, and a glyph outline is exactly the case it describes.
221
+ * A face describes a glyph as **SVG path data in em space**: one unit is one
222
+ * em, y grows downward, and the origin sits on the baseline at the glyph's pen
223
+ * position. That is independent of size, zoom, and position, so a glyph is
224
+ * tessellated once and transformed per instance.
217
225
  *
218
- * The string is also a natural cache key and costs nothing to hold: the
219
- * parse happens once per glyph, and core caches the *tessellation*, not the
220
- * text.
226
+ * `d` and not a command stream: `@weasel-js/core` depends on this package and
227
+ * never the reverse, so it cannot name core's `PolygonPath`, and SVG `d` is
228
+ * the kit's language for geometry crossing a boundary (`docs/conventions.md`).
221
229
  */
222
230
 
223
231
  /** A parsed font face, viewed only as a source of glyph outlines. See the
@@ -242,15 +250,12 @@ interface OutlineFace {
242
250
  faceMetrics?: FaceMetrics;
243
251
  /**
244
252
  * Em-space SVG path data for `cp`, or `null` when this face has no glyph
245
- * for the codepoint *or* the glyph has no contours (a space). Both answers
246
- * mean the same thing to a caller: there is nothing here to tessellate, so
247
- * fall through to whatever tier would have drawn it.
253
+ * for the codepoint *or* the glyph has no contours (a space).
248
254
  */
249
255
  glyphD(cp: number): string | null;
250
256
  /**
251
257
  * Advance width for `cp` in em units, or `null` when the face has no glyph
252
- * for it. Distinct from `glyphD` answering `null`: a space has an advance
253
- * and no contours, and layout needs the advance either way.
258
+ * for it. A space has an advance and no contours.
254
259
  */
255
260
  advanceOf(cp: number): number | null;
256
261
  /** Kerning adjustment between two codepoints, in em units — negative pulls
@@ -258,20 +263,14 @@ interface OutlineFace {
258
263
  kernOf(left: number, right: number): number;
259
264
  }
260
265
  /**
261
- * Turn font file bytes into a face. Async because the default implementation
262
- * dynamically imports its parser — see `opentypeParser.ts` for why that
263
- * matters — and because a caller may want to hand back a face that finishes
264
- * initializing off-thread.
266
+ * Turn font file bytes into a face. May be async: the default implementation
267
+ * dynamically imports its parser.
265
268
  */
266
269
  type OutlineParser = (bytes: ArrayBuffer) => OutlineFace | Promise<OutlineFace>;
267
270
 
268
271
  /**
269
- * FontRegistry and registerFont() public API.
270
- *
271
- * Variants are keyed by (family, weight, style). registerFont() takes a
272
- * FontVariant alongside the family and the two URLs; the registry stores
273
- * entries in a two-level Map so resolveFontVariant() can iterate a family's
274
- * variants for the fallback chain.
272
+ * The atlas font registry, keyed by (family, weight, style), and the fallback
273
+ * chain that resolves a request against it.
275
274
  */
276
275
 
277
276
  /** A registered face: its parsed metrics and the atlas image to sample. */
@@ -296,8 +295,7 @@ declare function getFont(family: string, weight?: number, style?: FontStyle): Fo
296
295
  /**
297
296
  * The decoration and script metrics of an exact `(family, weight, style)` —
298
297
  * the atlas's when one is registered, else a loaded outline face's. Like
299
- * {@link getFont}, it walks no fallback chain and enrolls no canvas font,
300
- * which suits a caller holding a CSS font name rather than a layout result.
298
+ * {@link getFont}, it walks no fallback chain and enrolls no canvas font.
301
299
  * An idle outline face starts loading, as it would for layout.
302
300
  * A run already resolved for layout reads `faceMetricsOf(result)` instead.
303
301
  */
@@ -312,17 +310,15 @@ interface RegisteredFont {
312
310
  }[];
313
311
  }
314
312
  /**
315
- * Enumerate the registry — what a font picker can honestly offer. Families
316
- * come back in registration order; variants sorted by weight, then style, so
317
- * the output is stable enough to assert against.
313
+ * Enumerate the registry. Families come back in registration order; variants
314
+ * sorted by weight, then style.
318
315
  */
319
316
  declare function listFonts(): readonly RegisteredFont[];
320
317
  /**
321
318
  * The weights `family` has a real face for, on the atlas tier or the outline
322
- * tier, in either style — what a weight picker can honestly offer. Ascending
323
- * and deduplicated; empty for a family nothing registered. An outline face
324
- * whose load failed is left out. The canvas tier is not consulted: it
325
- * rasterizes whatever weight it is asked for, so it has no list to report.
319
+ * tier, in either style. Ascending and deduplicated; empty for a family
320
+ * nothing registered. An outline face whose load failed is left out. The
321
+ * canvas tier is not consulted: it rasterizes whatever weight it is asked for.
326
322
  */
327
323
  declare function listFontWeights(family: string): readonly number[];
328
324
  /** Options for {@link registerFont}. */
@@ -342,8 +338,7 @@ interface RegisterFontOptions {
342
338
  * present is a no-op.
343
339
  *
344
340
  * Eager by default: both files are fetched now. With `{ lazy: true }` they are
345
- * fetched the first time text asks for the family, which keeps the atlas off
346
- * the critical path of anything that draws no text.
341
+ * fetched the first time text asks for the family.
347
342
  */
348
343
  declare function registerFont(family: string, variant: FontVariant, metricsUrl: string, atlasUrl: string, opts?: RegisterFontOptions): Promise<void>;
349
344
  /** One face text is set in, the way a render asks for it — what
@@ -367,21 +362,18 @@ interface FontRequest {
367
362
  * would draw instead when the requested one has no atlas; and the variant's
368
363
  * outline face when one is registered.
369
364
  *
370
- * Starts each lazily declared variant and joins any fetch already running.
371
365
  * Resolves once they have all landed; a registered variant counts as loaded.
372
366
  * Rejects when a load fails, or for a family name `registerFont` never saw —
373
- * a request for such a family is not an error, since the canvas and outline
374
- * tiers serve families no atlas was registered for.
367
+ * a {@link FontRequest} for such a family is not an error, since the canvas
368
+ * and outline tiers serve families no atlas was registered for.
375
369
  */
376
370
  declare function warmFonts(fonts?: readonly (string | FontRequest)[]): Promise<void>;
377
371
  /**
378
372
  * Could a registration that has not landed yet still serve this request?
379
373
  * True while any `registerFont` for the family is fetching — the within-family
380
374
  * chain may resolve to whichever variant it brings — or while an outline face
381
- * registered for this exact variant has not finished loading.
382
- *
383
- * A face that resolves to nothing right now is either missing or merely late;
384
- * this tells the two apart, so "not registered" is reported only when it is.
375
+ * registered for this exact variant has not finished loading. Tells a face
376
+ * that is missing from one that is merely late.
385
377
  */
386
378
  declare function fontPending(family: string, weight?: number, style?: FontStyle): boolean;
387
379
  /**
@@ -405,9 +397,8 @@ interface ResolveResult {
405
397
  * including `family`, when the cross-family policy substituted a default.
406
398
  *
407
399
  * This is the atlas identity: pass all three to `getFont` /
408
- * `textureCacheKey` and the lookup hits. Describing only weight and style
409
- * here once let a caller key its draw on the *requested* family, which
410
- * resolves to no atlas at all and paints nothing.
400
+ * `textureCacheKey` and the lookup hits. Keying a draw on the *requested*
401
+ * family resolves to no atlas at all and paints nothing.
411
402
  *
412
403
  * The synthetic flags describe the gap between requested and resolved for
413
404
  * shader-side compensation. When `entry` is null, these mirror the
@@ -435,9 +426,8 @@ interface ResolveResult {
435
426
  outlineFace?: OutlineFace;
436
427
  /**
437
428
  * Set when the requested family was not registered and the fallback policy
438
- * substituted a different one. Reported structurally so a UI can say
439
- * "Inter — not loaded, showing Roboto" instead of leaving the user to
440
- * wonder why the family control did nothing.
429
+ * substituted a different one, so a UI can say "Inter — not loaded, showing
430
+ * Roboto".
441
431
  */
442
432
  substituted?: {
443
433
  requested: string;
@@ -455,23 +445,14 @@ declare function resolveFontVariant(family: string, weight: number, style: FontS
455
445
  * Escalate a *single codepoint* to the dynamic tier when the atlas that
456
446
  * resolved for the run has no glyph for it.
457
447
  *
458
- * `resolveFontVariant` answers at family granularity: it picks one tier for a
459
- * whole run. But a baked MSDF atlas covers a fixed charset, so a run served by
460
- * a perfectly good atlas can still contain a character that atlas never baked
461
- * — an em dash, a curly quote, anything outside the subset. The dynamic tier
462
- * rasterizes on demand from installed fonts and can serve exactly those.
448
+ * A baked MSDF atlas covers a fixed charset, so a run it serves can still
449
+ * contain a character it never baked — an em dash, a curly quote. The dynamic
450
+ * tier rasterizes on demand from installed fonts and can serve those.
463
451
  *
464
452
  * Returns a canvas-tier `ResolveResult` whose `dynamicFace` the caller drives
465
- * with `requestGlyph(cp)`, or `null` when escalation isn't available:
466
- *
467
- * - Policy `'none'` documents a miss as a *hard* miss. Quietly reaching for
468
- * another tier per codepoint would undo that, so it doesn't.
469
- * - No canvas to rasterize into (SSR, a jsdom test without one) makes the
470
- * dynamic tier constructible-but-broken; `getDynamicFace` throws and this
471
- * reports the miss instead of taking the caller down with it.
472
- *
473
- * Cheap to call per missing codepoint: faces are cached by variant and glyphs
474
- * by codepoint, so a repeat is two map lookups.
453
+ * with `requestGlyph(cp)`, or `null` when escalation isn't available: under
454
+ * policy `'none'`, which documents a miss as a *hard* miss, or with no canvas
455
+ * to rasterize into (SSR, a jsdom test without one).
475
456
  */
476
457
  declare function resolveGlyphFallback(family: string, weight: number, style: FontStyle): ResolveResult | null;
477
458
 
@@ -481,10 +462,8 @@ declare function resolveGlyphFallback(family: string, weight: number, style: Fon
481
462
  * baseline to its top edge (y grows down) and a script's `shift` positive
482
463
  * for a rise.
483
464
  *
484
- * Both glyph tiers carry the same record — an atlas bakes it into its JSON,
485
- * a parsed face reads it off the font's tables — and both derive it through
486
- * {@link faceMetricsFromTables}, so one font yields one set of numbers
487
- * whichever tier ends up serving a run.
465
+ * Both glyph tiers derive the record through {@link faceMetricsFromTables},
466
+ * so one font yields one set of numbers whichever tier serves a run.
488
467
  */
489
468
 
490
469
  /** One rule: the top edge's distance below the baseline, and its weight. */
@@ -552,12 +531,10 @@ interface FaceMetricTables {
552
531
  * positive downward.
553
532
  *
554
533
  * The OpenType rule: `OS/2` `sTypoAscender`/`sTypoDescender` when the font
555
- * sets `USE_TYPO_METRICS`, otherwise `hhea`. It is the browser's rule rather
556
- * than a typographer's because the DOM edit overlay stands in for the canvas
557
- * and can only ever get the browser's numbers. Firefox, and every engine on
534
+ * sets `USE_TYPO_METRICS`, otherwise `hhea`. Firefox, and every engine on
558
535
  * Linux, follow it; Chromium and WebKit on macOS read `hhea` even when the
559
- * flag is set, and the overlay's measured baseline correction absorbs the
560
- * difference there. `verticalMetrics.browser.test.ts` pins both.
536
+ * flag is set, and the DOM edit overlay's measured baseline correction
537
+ * absorbs the difference there.
561
538
  *
562
539
  * Falls back to the typo pair, then the win pair, when `hhea` is absent or
563
540
  * zero.
@@ -572,16 +549,14 @@ declare function verticalMetricsFromTables(t: FaceMetricTables): {
572
549
  * `post.underlinePosition` and `OS/2.yStrikeoutPosition` are the rule's top
573
550
  * edge, positive above the baseline. `ySubscriptYOffset` is positive
574
551
  * *downward*, unlike its superscript twin. A zero weight or size is a font
575
- * that left the field blank, not one asking for an invisible rule, so that
576
- * entry is dropped and the caller's default applies — which is also how an
577
- * `OS/2` table older than version 2 reads, since it has no heights at all.
552
+ * that left the field blank, so that entry is dropped and the caller's
553
+ * default applies.
578
554
  */
579
555
  declare function faceMetricsFromTables(t: FaceMetricTables): FaceMetrics | undefined;
580
556
  /**
581
557
  * The face metrics behind a resolved run — the outline face's on the outline
582
- * tier, the atlas's on the atlas tier. The canvas tier rasterizes through
583
- * `CanvasRenderingContext2D`, which exposes no font tables, so it has none
584
- * and callers fall back to their defaults.
558
+ * tier, the atlas's on the atlas tier. The canvas tier has none, so callers
559
+ * fall back to their defaults.
585
560
  */
586
561
  declare function faceMetricsOf(result: ResolveResult): FaceMetrics | undefined;
587
562
 
@@ -616,6 +591,9 @@ interface BmFontChar {
616
591
  yoffset: number;
617
592
  xadvance: number;
618
593
  page: number;
594
+ /** Set on a glyph whose atlas rect holds its own colors, on a page of the
595
+ * dynamic tier's color set: an emoji. It is painted as it is, untinted. */
596
+ color?: true;
619
597
  }
620
598
  /** A kerning pair: `amount` px to add to the advance when `second` follows
621
599
  * `first`. Codepoints, not glyph indices. */
@@ -716,13 +694,9 @@ declare function _resetFallbackForTests(): void;
716
694
  * sans-serif` in the page resolves to Helvetica. Anything the DOM sets in
717
695
  * place of canvas text — the text edit overlay above all — asks here instead
718
696
  * of writing the name through. The answer names a private `FontFace` loaded
719
- * from the family's registered font file, with the family itself as fallback
720
- * while that loads.
721
- *
722
- * The font file is the one `registerFontOutlines` holds; that registry is the
723
- * kit's record of which faces have real bytes behind them. An atlas-only
724
- * family has none, so the DOM gets the browser's own face for the name and a
725
- * one-time warning says which registration would fix it.
697
+ * from the file `registerFontOutlines` holds, with the family itself as
698
+ * fallback while that loads. An atlas-only family has no file, so the DOM gets
699
+ * the browser's own face for the name.
726
700
  */
727
701
 
728
702
  /**
@@ -733,15 +707,13 @@ declare function _resetFallbackForTests(): void;
733
707
  * the `'canvas'` fallback policy) comes back unchanged: the DOM already
734
708
  * resolves the name to the same face.
735
709
  * - A family drawn from a baked atlas or from outlines comes back as a private
736
- * face built from its `registerFontOutlines` file — every registered weight
737
- * and style of it, so the browser picks and synthesizes within the family
738
- * the way the canvas does — followed by `family` as the fallback.
710
+ * face built from its `registerFontOutlines` file, in every registered
711
+ * weight and style, followed by `family` as the fallback.
739
712
  * - Under the `'substitute'` policy, the face is the substitute's.
740
713
  *
741
714
  * Starts loading the face on first call and never waits for it; ask
742
- * {@link cssFontFamilyLoading} for that. A face whose bytes arrived earlier —
743
- * passed to `registerFontOutlines` as an `ArrayBuffer`, or read for the canvas
744
- * — was built when they did. Outside a browser, `family` comes back unchanged.
715
+ * {@link cssFontFamilyLoading} for that. Outside a browser, `family` comes
716
+ * back unchanged.
745
717
  */
746
718
  declare function cssFontFamily(family: string, variant?: FontVariant): string;
747
719
  /**
@@ -765,57 +737,35 @@ declare function _resetDomFacesForTests(): void;
765
737
  *
766
738
  * ### What this tier is for
767
739
  *
768
- * A distance field is a *sampled* representation. The baked MSDF atlas is
769
- * generated from outlines and holds up well; the dynamic canvas-SDF tier
740
+ * A distance field is a *sampled* representation: the dynamic canvas-SDF tier
770
741
  * (`../dynamic/`) reconstructs its field from a 48px raster, so magnifying it
771
- * past a few times the bake size exposes the raster as contour wobble — bumps
772
- * one bake-texel apart, ±2–3px at 8×. No bake size fixes that, it only moves
773
- * the size at which it appears (`glyphRasterizer.ts` measures the tradeoff:
774
- * error is *minimized at* the bake size, so raising it spoils the 12–32px
775
- * range where most text lives).
776
- *
777
- * Above a size threshold the answer is to stop sampling: parse the font, take
778
- * the glyph's outline, and hand it to the path renderer the kit already has.
779
- * Exact at every zoom, and — because a glyph becomes an ordinary path — it
780
- * takes strokes and non-solid fills for free.
781
- *
782
- * Below the threshold the atlas still wins, and not narrowly: outlines carry
783
- * no hinting and no stem darkening, so body text at 12–16px rendered from
784
- * them looks *worse* than a platform rasterizer that puts stems on the pixel
785
- * grid. Two tiers with a size threshold is the standard hybrid, not a
786
- * compromise.
742
+ * past a few times the bake size exposes the raster as contour wobble. Above a
743
+ * size threshold the glyph's outline goes to the path renderer instead, which
744
+ * is exact at every zoom and takes strokes and non-solid fills.
745
+ *
746
+ * Below the threshold the atlas still wins: outlines carry no hinting and no
747
+ * stem darkening, so body text at 12–16px rendered from them looks *worse*
748
+ * than a platform rasterizer that puts stems on the pixel grid.
787
749
  *
788
750
  * ### Metric neutrality (the load-bearing invariant)
789
751
  *
790
752
  * This tier replaces how a glyph is *painted*, never where it sits. Advances,
791
- * kerning, line breaking and baselines all keep coming from whichever tier
792
- * resolved the run — the baked atlas or the canvas face. That is what lets
793
- * the threshold be a rendering decision: zooming past it swaps glyph geometry
794
- * with the layout untouched, so text cannot reflow under the user's cursor,
795
- * and `measureTextBounds` / `textLineBoxes` need to know nothing about
796
- * outlines at all.
797
- *
798
- * The cost is that a face whose real advances disagree with the atlas's
799
- * inherits the atlas's. For the bundled Inter that is exact (the subset ships
800
- * from the same source the atlas was baked from), and for a machine font the
801
- * outline bytes and the canvas metrics come from the same file.
753
+ * kerning, line breaking, and baselines all keep coming from whichever tier
754
+ * resolved the run, so zooming past the threshold cannot reflow text. The cost
755
+ * is that a face whose real advances disagree with the atlas's inherits the
756
+ * atlas's.
802
757
  *
803
758
  * ### Availability
804
759
  *
805
- * Everything here degrades rather than blocks. Bytes arrive over `fetch` or
806
- * `queryLocalFonts` — asynchronous, and in the local case gated behind a
807
- * permission the user can refuse — so `glyphOutline` answers `null` until a
808
- * face is loaded and forever if it failed, and the caller falls back down the
809
- * ladder to SDF. A family must never render *nothing* because the outline
810
- * tier could not get bytes.
760
+ * Bytes arrive asynchronously, and in the local case behind a permission the
761
+ * user can refuse, so `glyphOutline` answers `null` until a face is loaded and
762
+ * forever if it failed, and the caller falls back to SDF. A family must never
763
+ * render *nothing* because the outline tier could not get bytes.
811
764
  */
812
765
 
813
766
  /**
814
767
  * Where a face's bytes come from. A URL is fetched; a buffer or blob is used
815
- * as-is; a thunk is called at first use, which is what lets `queryLocalFonts`
816
- * results be registered eagerly and read lazily — the permission prompt and
817
- * the (potentially many megabytes of) blob only happen for a family the
818
- * document actually sets text in.
768
+ * as-is; a thunk is called at first use.
819
769
  */
820
770
  type OutlineSource = string | ArrayBuffer | Blob | (() => ArrayBuffer | Blob | Promise<ArrayBuffer | Blob>);
821
771
  /** Which face within a family to register outlines for. Defaults to weight
@@ -855,13 +805,12 @@ declare const fontOutlineRegistry: Reflection<OutlineFaceInfo>;
855
805
  *
856
806
  * Registration is cheap and synchronous: nothing is fetched, and the parser
857
807
  * is not even imported, until a frame actually asks for a glyph from this
858
- * face. Registering a face that is never drawn large costs one map entry.
808
+ * face.
859
809
  *
860
810
  * The variant must match exactly what the layout tier resolved — this is
861
811
  * deliberately *not* a fallback chain. Painting 400-weight outlines at
862
- * 700-weight advances, or upright outlines where the shader was going to
863
- * fake an oblique, looks worse than the SDF it replaced; a miss here falls
864
- * back down the ladder instead, which is always safe.
812
+ * 700-weight advances looks worse than the SDF it replaced; a miss falls back
813
+ * to SDF instead.
865
814
  */
866
815
  declare function registerFontOutlines(family: string, variant: OutlineVariant, source: OutlineSource, opts?: OutlineFontOptions): void;
867
816
  /** Drop a registration. Glyphs already handed out stay valid — they are
@@ -869,9 +818,7 @@ declare function registerFontOutlines(family: string, variant: OutlineVariant, s
869
818
  declare function unregisterFontOutlines(family: string, variant?: OutlineVariant): void;
870
819
  /**
871
820
  * Is an outline face registered for this exact variant? Answers `true` while
872
- * the bytes are still loading — the question is "could this face serve
873
- * outlines", which is what a caller deciding whether to *ask* wants; use
874
- * `outlineStatus` for the narrower "can it right now".
821
+ * the bytes are still loading; use `outlineStatus` for "can it right now".
875
822
  */
876
823
  declare function hasFontOutlines(family: string, weight?: number, style?: FontStyle): boolean;
877
824
  /** Load state of a registered face; `null` when nothing is registered. */
@@ -881,41 +828,34 @@ declare function outlineStatus(family: string, weight?: number, style?: FontStyl
881
828
  declare function listFontOutlines(): readonly OutlineFaceInfo[];
882
829
  /**
883
830
  * Em-space SVG path data for one glyph of a registered outline face, or `null`
884
- * when there is nothing to draw.
831
+ * when this tier cannot serve it — no registration, bytes still loading, load
832
+ * failed, no such glyph, or a glyph with no contours.
885
833
  *
886
- * Synchronous and non-throwing by design: a face that has not been parsed yet
887
- * starts loading and answers `null` for now, so a renderer can call this every
888
- * frame and simply fall through to another text tier until the outline
889
- * arrives. Results are cached per codepoint.
834
+ * Synchronous and non-throwing: a face that has not been parsed yet starts
835
+ * loading and answers `null` for now, so a renderer can call this every frame
836
+ * and fall through to another text tier until the outline arrives.
890
837
  */
891
838
  declare function glyphOutline(family: string, weight: number, style: FontStyle, cp: number): string | null;
892
839
  /**
893
840
  * Metrics for a registered outline face, or `null` when this tier cannot
894
841
  * supply them yet — nothing registered, bytes still loading, or a failed load.
895
842
  *
896
- * The face is the *only* metrics source for a family that has no atlas, which
897
- * is the case this exists for: without it such a family cannot lay out at all.
898
- * It does not disturb metric neutrality — a family that has an atlas resolves
899
- * to the atlas long before anything asks here, so registering outlines still
900
- * cannot move text that was already rendering.
843
+ * The face is the *only* metrics source for a family that has no atlas. A
844
+ * family that has an atlas resolves to the atlas before anything asks here,
845
+ * so registering outlines cannot move text that was already rendering.
901
846
  *
902
- * Starts the load on a first call and answers `null` for now, exactly like
903
- * `glyphOutline`, so a per-frame caller falls through and picks it up on the
904
- * frame after the bytes land.
847
+ * Starts the load on a first call and answers `null` for now, like
848
+ * `glyphOutline`.
905
849
  */
906
850
  declare function outlineMetrics(family: string, weight: number, style: FontStyle): OutlineFace | null;
907
851
  /**
908
852
  * Load a registered face's bytes now and resolve with its settled status:
909
853
  * `'ready'`, `'failed'`, or `null` when nothing is registered for the
910
- * variant. Never rejects — a failed load is a status, the same one
911
- * `outlineStatus` reports.
912
- *
913
- * The per-frame readers start a load and answer `null` until it lands, which
914
- * suits a renderer and nothing that needs the geometry in one go: converting
915
- * text to paths awaits this first.
854
+ * variant. Never rejects — a failed load is a status. For a caller that needs
855
+ * the geometry in one go, such as converting text to paths.
916
856
  */
917
857
  declare function loadFontOutlines(family: string, variant?: OutlineVariant): Promise<OutlineStatus | null>;
918
858
  /** @internal Test seam — registry and warn-once keys are module state. */
919
859
  declare function _resetFontOutlinesForTests(): void;
920
860
 
921
- export { outlineStatus as $, faceMetricsFor as A, type BmFont as B, type CanvasFontEntry as C, DEFAULT_BAKE_BUDGET as D, faceMetricsFromTables as E, type FontStyle as F, type GlyphTextureSink as G, faceMetricsOf as H, fontOutlineRegistry as I, fontPending as J, fontRegistry as K, getDefaultFontFamily as L, getFont as M, getFontFallbackPolicy as N, type OutlineFace as O, glyphOutline as P, hasFontOutlines as Q, type RegisterFontOptions as R, isCanvasFont as S, type TexSource as T, listCanvasFonts as U, listFontOutlines as V, listFontWeights as W, listFonts as X, loadFontOutlines as Y, markAllFontsNotUploaded as Z, outlineMetrics as _, type BmFontChar as a, parseBmFont as a0, registerCanvasFont as a1, registerFont as a2, registerFontOutlines as a3, resetBakeBudget as a4, resolveFontVariant as a5, resolveGlyphFallback as a6, setDefaultFontFamily as a7, setFontFallbackPolicy as a8, syncDynamicPageTexture as a9, textureCacheKey as aa, unregisterCanvasFont as ab, unregisterFontOutlines as ac, verticalMetricsFromTables as ad, warmFonts as ae, __setGlyphRasterizerForTests as af, _getPagesForTests as ag, _resetDomFacesForTests as ah, _resetDynamicFontsForTests as ai, _resetFallbackForTests as aj, _resetFontOutlinesForTests as ak, _resetFontRegistryForTests as al, type BmFontCommon as b, type BmFontInfo as c, type BmFontKerning as d, FIXTURE_FONT as e, type FaceMetricTables as f, type FaceMetrics as g, type FaceRuleMetrics as h, type FaceScriptMetrics as i, type FontEntry as j, type FontFallbackPolicy as k, type FontFamilyFaces as l, type FontRequest as m, type FontVariant as n, type OutlineFaceInfo as o, type OutlineFontOptions as p, type OutlineParser as q, type OutlineSource as r, type OutlineStatus as s, type OutlineVariant as t, type RegisteredFont as u, type ResolveResult as v, cssFontFamily as w, cssFontFamilyLoading as x, dynamicPageTextureId as y, ensureFontTexture as z };
861
+ export { outlineMetrics as $, ensureFontTexture as A, type BmFont as B, type CanvasFontEntry as C, type DynamicFace as D, faceMetricsFor as E, type FontStyle as F, type GlyphTextureSink as G, faceMetricsFromTables as H, faceMetricsOf as I, fontOutlineRegistry as J, fontPending as K, fontRegistry as L, getDefaultFontFamily as M, getFont as N, type OutlineFace as O, getFontFallbackPolicy as P, glyphOutline as Q, type ResolveResult as R, hasFontOutlines as S, type TexSource as T, isCanvasFont as U, listCanvasFonts as V, listFontOutlines as W, listFontWeights as X, listFonts as Y, loadFontOutlines as Z, markAllFontsNotUploaded as _, type BmFontChar as a, outlineStatus as a0, parseBmFont as a1, refreshCanvasFont as a2, registerCanvasFont as a3, registerFont as a4, registerFontOutlines as a5, resolveFontVariant as a6, resolveGlyphFallback as a7, setDefaultFontFamily as a8, setFontFallbackPolicy as a9, syncColorPageTexture as aa, syncDynamicPageTexture as ab, textureCacheKey as ac, unregisterCanvasFont as ad, unregisterFontOutlines as ae, verticalMetricsFromTables as af, warmFonts as ag, __setGlyphRasterizerForTests as ah, _getPagesForTests as ai, _resetDomFacesForTests as aj, _resetDynamicFontsForTests as ak, _resetFallbackForTests as al, _resetFontOutlinesForTests as am, _resetFontRegistryForTests as an, type BmFontCommon as b, type BmFontInfo as c, type BmFontKerning as d, FIXTURE_FONT as e, type FaceMetricTables as f, type FaceMetrics as g, type FaceRuleMetrics as h, type FaceScriptMetrics as i, type FontEntry as j, type FontFallbackPolicy as k, type FontFamilyFaces as l, type FontRequest as m, type FontVariant as n, type OutlineFaceInfo as o, type OutlineFontOptions as p, type OutlineParser as q, type OutlineSource as r, type OutlineStatus as s, type OutlineVariant as t, type RegisterFontOptions as u, type RegisteredFont as v, colorPageTextureId as w, cssFontFamily as x, cssFontFamilyLoading as y, dynamicPageTextureId as z };