@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.
package/dist/index.d.ts CHANGED
@@ -1,33 +1,32 @@
1
- import { F as FontStyle } from './test-seams-BC5Ykf4D.js';
2
- export { B as BmFont, a as BmFontChar, b as BmFontCommon, c as BmFontInfo, d as BmFontKerning, C as CanvasFontEntry, D as DEFAULT_BAKE_BUDGET, e as FIXTURE_FONT, f as FaceMetricTables, g as FaceMetrics, h as FaceRuleMetrics, i as FaceScriptMetrics, j as FontEntry, k as FontFallbackPolicy, l as FontFamilyFaces, m as FontRequest, n as FontVariant, G as GlyphTextureSink, O as OutlineFace, o as OutlineFaceInfo, p as OutlineFontOptions, q as OutlineParser, r as OutlineSource, s as OutlineStatus, t as OutlineVariant, R as RegisterFontOptions, u as RegisteredFont, v as ResolveResult, T as TexSource, w as cssFontFamily, x as cssFontFamilyLoading, y as dynamicPageTextureId, z as ensureFontTexture, A as faceMetricsFor, E as faceMetricsFromTables, H as faceMetricsOf, I as fontOutlineRegistry, J as fontPending, K as fontRegistry, L as getDefaultFontFamily, M as getFont, N as getFontFallbackPolicy, P as glyphOutline, Q as hasFontOutlines, S as isCanvasFont, U as listCanvasFonts, V as listFontOutlines, W as listFontWeights, X as listFonts, Y as loadFontOutlines, Z as markAllFontsNotUploaded, _ as outlineMetrics, $ as outlineStatus, a0 as parseBmFont, a1 as registerCanvasFont, a2 as registerFont, a3 as registerFontOutlines, a4 as resetBakeBudget, a5 as resolveFontVariant, a6 as resolveGlyphFallback, a7 as setDefaultFontFamily, a8 as setFontFallbackPolicy, a9 as syncDynamicPageTexture, aa as textureCacheKey, ab as unregisterCanvasFont, ac as unregisterFontOutlines, ad as verticalMetricsFromTables, ae as warmFonts } from './test-seams-BC5Ykf4D.js';
1
+ import { R as ResolveResult, B as BmFont, a as BmFontChar, F as FontStyle } from './test-seams-BvULFsbo.js';
2
+ export { b as BmFontCommon, c as BmFontInfo, d as BmFontKerning, C as CanvasFontEntry, D as DynamicFace, e as FIXTURE_FONT, f as FaceMetricTables, g as FaceMetrics, h as FaceRuleMetrics, i as FaceScriptMetrics, j as FontEntry, k as FontFallbackPolicy, l as FontFamilyFaces, m as FontRequest, n as FontVariant, G as GlyphTextureSink, O as OutlineFace, o as OutlineFaceInfo, p as OutlineFontOptions, q as OutlineParser, r as OutlineSource, s as OutlineStatus, t as OutlineVariant, u as RegisterFontOptions, v as RegisteredFont, T as TexSource, w as colorPageTextureId, x as cssFontFamily, y as cssFontFamilyLoading, z as dynamicPageTextureId, A as ensureFontTexture, E as faceMetricsFor, H as faceMetricsFromTables, I as faceMetricsOf, J as fontOutlineRegistry, K as fontPending, L as fontRegistry, M as getDefaultFontFamily, N as getFont, P as getFontFallbackPolicy, Q as glyphOutline, S as hasFontOutlines, U as isCanvasFont, V as listCanvasFonts, W as listFontOutlines, X as listFontWeights, Y as listFonts, Z as loadFontOutlines, _ as markAllFontsNotUploaded, $ as outlineMetrics, a0 as outlineStatus, a1 as parseBmFont, a2 as refreshCanvasFont, a3 as registerCanvasFont, a4 as registerFont, a5 as registerFontOutlines, a6 as resolveFontVariant, a7 as resolveGlyphFallback, a8 as setDefaultFontFamily, a9 as setFontFallbackPolicy, aa as syncColorPageTexture, ab as syncDynamicPageTexture, ac as textureCacheKey, ad as unregisterCanvasFont, ae as unregisterFontOutlines, af as verticalMetricsFromTables, ag as warmFonts } from './test-seams-BvULFsbo.js';
3
3
  import '@weasel-js/registry';
4
4
 
5
+ /**
6
+ * Baking measured glyphs into the dynamic atlas's pages: a few a frame, the
7
+ * rest on later ticks, and every page emptied whole when one set fills.
8
+ */
9
+
10
+ /** How many glyphs the dynamic atlas will rasterize synchronously before
11
+ * deferring the rest to a later flush. Caps the frame cost of text that
12
+ * suddenly needs many new glyphs; the leftovers arrive a tick later and
13
+ * notify through `onGlyphReady`. */
14
+ declare const DEFAULT_BAKE_BUDGET = 16;
15
+ /** Reset the per-frame synchronous bake budget. Called by
16
+ * `WeaselRenderer.render()` at frame start; the headless
17
+ * `renderSceneToPixels` path passes Infinity so print never defers. */
18
+ declare function resetBakeBudget(n?: number): void;
19
+
5
20
  /**
6
21
  * "A glyph the renderer asked for can now paint" — the redraw signal shared
7
- * by every asynchronous glyph source.
8
- *
9
- * Both lazy tiers land their work *after* the frame that asked for it: the
10
- * dynamic canvas-SDF atlas defers bakes past a per-frame budget, and the
11
- * outline tier has to fetch and parse font bytes. Neither can paint anything
12
- * on the frame that first requests a glyph, so both have to tell the canvas
13
- * to draw again.
14
- *
15
- * One subscriber set, not one per tier: a consumer wiring "redraw when text
16
- * becomes drawable" should not have to know how many tiers exist, or grow a
17
- * second subscription every time one is added. `<SceneCanvas>` subscribes
18
- * once and both tiers reach it.
22
+ * by every asynchronous glyph source. Both lazy tiers land their work after
23
+ * the frame that asked for it, so both have to tell the canvas to draw again.
19
24
  */
20
25
  /**
21
26
  * How many times glyph availability has changed — the pull-based companion to
22
- * {@link subscribeGlyphReady}.
23
- *
24
- * For caches that cannot hold a subscription. Subscribing at module load is a
25
- * side effect that reaches across a package boundary: it makes importing the
26
- * module that holds the cache require this package's whole surface, so a
27
- * consumer partially mocking `@weasel-js/font` breaks at import rather than at
28
- * use, and import order starts to matter. Comparing a counter has neither
29
- * problem, and costs a field read on a path that was going to check a cache
30
- * key anyway.
27
+ * {@link subscribeGlyphReady}, for caches that cannot hold a subscription.
28
+ * Subscribing at module load makes a consumer partially mocking
29
+ * `@weasel-js/font` break at import rather than at use.
31
30
  *
32
31
  * Callers keep the last value they saw and compare; any difference means
33
32
  * anything derived from font metrics is stale. The absolute value carries no
@@ -42,6 +41,15 @@ declare function glyphGeneration(): number;
42
41
  * either way. Returns an unsubscribe.
43
42
  */
44
43
  declare function subscribeGlyphReady(cb: () => void): () => void;
44
+ /**
45
+ * How many times the faces themselves have changed: a registration, a load
46
+ * landing or failing, a fallback policy, a canvas face measured again or its
47
+ * pages emptied. Unlike {@link glyphGeneration} it holds still when a deferred
48
+ * bake lands, which fills in an atlas rect and moves no pen — so text measured
49
+ * under one value is still measured right while the value stands. Compare
50
+ * with `!==`.
51
+ */
52
+ declare function faceGeneration(): number;
45
53
 
46
54
  /**
47
55
  * `family` as it must appear in a CSS `font` shorthand or `font-family`.
@@ -54,6 +62,87 @@ declare function subscribeGlyphReady(cb: () => void): () => void;
54
62
  */
55
63
  declare function cssFamilyName(family: string): string;
56
64
 
65
+ /**
66
+ * One shape for the advances, kerning and baseline of whichever tier resolved
67
+ * a face. A baked atlas and the canvas tier measure in their bake-size pixels
68
+ * and a parsed face in ems; `size` is the divisor that takes any of them to
69
+ * world units (`fontSize / size`), which is why an em-space face reports 1.
70
+ */
71
+
72
+ /** Where a face's advances, kerning and baseline come from. */
73
+ interface GlyphMetrics {
74
+ /** Units per em of everything else here. */
75
+ readonly size: number;
76
+ /** Line top to baseline in the glyph frame: what an atlas glyph's `yoffset`
77
+ * is measured against. */
78
+ readonly base: number;
79
+ /** Baseline to the top of the face's line box: the ascent a browser sets
80
+ * the face with, or `base` for a face that states none. */
81
+ readonly ascent: number;
82
+ /** Baseline to the bottom of the face's line box. Absent for a face that
83
+ * states no ascent and descent, which hangs its baseline `base` below the
84
+ * line top instead of centering the face in its line. */
85
+ readonly descent?: number;
86
+ /** Advance for `cp`, or null when the face has no glyph for it. On the
87
+ * canvas tier this measures the glyph and queues its bake. */
88
+ advanceOf(cp: number): number | null;
89
+ kernOf(left: number, right: number): number;
90
+ }
91
+ /**
92
+ * The metrics of whichever tier `result` resolved to, or `undefined` for a
93
+ * miss, which has none. One object per face, so two results that landed on the
94
+ * same face compare equal by identity.
95
+ */
96
+ declare function metricsOf(result: ResolveResult): GlyphMetrics | undefined;
97
+
98
+ /**
99
+ * Finding the glyph for one code point: in the tier a run resolved to, then
100
+ * in the dynamic tier, then nowhere. A miss never draws a stand-in `?`, which
101
+ * is indistinguishable from one the author wrote, and the BmFont atlas format
102
+ * has no `.notdef` to draw.
103
+ */
104
+
105
+ /** A code point's glyph, and the tier that ended up serving it. */
106
+ interface ResolvedGlyph {
107
+ /** The tier serving the glyph. Not the run's own when the code point
108
+ * escalated to the dynamic tier. */
109
+ resolved: ResolveResult;
110
+ /** That tier's metrics, which are what `advance` is measured in. */
111
+ metrics: GlyphMetrics;
112
+ advance: number;
113
+ /** The atlas to sample and the glyph's record in it. Both null on the
114
+ * outline tier, which has geometry instead. A canvas glyph's record gains
115
+ * its atlas rect in place when its bake lands. */
116
+ font: BmFont | null;
117
+ glyph: BmFontChar | null;
118
+ }
119
+ /** The glyph for `cp` in the tier `resolved` landed on, or null when that
120
+ * tier has none. */
121
+ declare function glyphIn(resolved: ResolveResult, cp: number): ResolvedGlyph | null;
122
+ /**
123
+ * The glyph for `cp` in a run that asked for `(family, weight, style)` and
124
+ * resolved to `resolved`: that tier's own, else the dynamic tier's under the
125
+ * face the run asked for. Null when neither has it, with a warning naming the
126
+ * code point unless `warn` is false.
127
+ */
128
+ declare function resolveGlyph(resolved: ResolveResult, family: string, weight: number, style: FontStyle, cp: number, warn?: boolean): ResolvedGlyph | null;
129
+ /**
130
+ * The color glyph for a whole grapheme cluster with emoji presentation, in a
131
+ * run that asked for `(family, weight, style)` and resolved to `resolved`.
132
+ * The canvas tier is the only one that draws a cluster as one glyph or in its
133
+ * own colors, so it serves these whatever tier serves the run's text. Null for
134
+ * any other cluster, and where the canvas tier is not to be had or draws no
135
+ * color for this one: the cluster is then laid out code point by code point.
136
+ */
137
+ declare function resolveColorCluster(resolved: ResolveResult, family: string, weight: number, style: FontStyle, cluster: string): ResolvedGlyph | null;
138
+
139
+ /**
140
+ * Say, once per face, that text set in it has no metrics and lays out as
141
+ * nothing. Silent while the face is still loading, which is late rather than
142
+ * missing, and when the registry has already said why it cannot render.
143
+ */
144
+ declare function warnNoMetrics(family: string, weight: number, style: FontStyle): void;
145
+
57
146
  /**
58
147
  * Local Font Access → outline registrations.
59
148
  *
@@ -64,17 +153,13 @@ declare function cssFamilyName(family: string): string;
64
153
  * and gated behind a permission prompt that requires a user gesture.
65
154
  *
66
155
  * So this is the one part of the tier a consumer has to *ask* for, from a
67
- * click. Everything downstream treats a refusal as ordinary: no
68
- * registrations, `glyphOutline` keeps answering `null`, and large text keeps
69
- * rendering from the SDF tier exactly as it does today. That ladder is
70
- * load-bearing rather than defensive — Safari and Firefox have no equivalent
71
- * API at all, so "denied" is the permanent state for most of the web.
156
+ * click. A refusal is ordinary: no registrations, and large text keeps
157
+ * rendering from the SDF tier. Safari and Firefox have no equivalent API at
158
+ * all, so "denied" is the permanent state for most of the web.
72
159
  *
73
160
  * Registration is eager but reading is lazy: every matching face gets a thunk
74
- * that calls `FontData.blob()` at first glyph request. A machine can easily
75
- * have hundreds of faces and a `.ttc` runs to tens of megabytes, so
76
- * downloading them all at enable time would be worse than not offering the
77
- * tier.
161
+ * that calls `FontData.blob()` at first glyph request. A machine can have
162
+ * hundreds of faces and a `.ttc` runs to tens of megabytes.
78
163
  */
79
164
 
80
165
  /** Is the Local Font Access API present at all? False on every non-Chromium
@@ -84,26 +169,21 @@ declare function canQueryLocalFonts(): boolean;
84
169
  * Parse a `FontData.style` string — "Regular", "Bold Italic", "Condensed
85
170
  * ExtraLight Oblique" — into the CSS variant the kit's registry is keyed by.
86
171
  *
87
- * Anything unrecognized lands on 400/normal, which is what the string
88
- * "Regular" would have produced anyway. A wrong guess here costs a face that
89
- * registers under a variant nothing asks for; it cannot mis-paint anything,
90
- * because resolution is an exact-match lookup that simply misses.
172
+ * Anything unrecognized lands on 400/normal.
91
173
  */
92
174
  declare function parseFontStyle(style: string): {
93
175
  weight: number;
94
176
  style: FontStyle;
95
177
  };
96
- /** Options for `registerLocalFontOutlines`. */
178
+ /** Options for `enableLocalFontOutlines`. */
97
179
  interface LocalFontOutlinesOptions {
98
180
  /**
99
181
  * Restrict registration to these families. Defaults to every installed
100
- * family. Worth passing when the consumer already knows its font menu:
101
- * a machine with 900 faces otherwise gets 900 registry entries, all inert
102
- * but all held.
182
+ * family, which can be hundreds of registry entries.
103
183
  */
104
184
  families?: readonly string[];
105
185
  }
106
- /** What `registerLocalFontOutlines` registered. */
186
+ /** What `enableLocalFontOutlines` registered. */
107
187
  interface LocalFontOutlinesResult {
108
188
  /** Families that now have at least one outline face, in menu order. */
109
189
  families: readonly string[];
@@ -115,9 +195,8 @@ interface LocalFontOutlinesResult {
115
195
  * for the faces it returns. **Must be called from a user gesture** — the
116
196
  * permission prompt requires one, and without it the call rejects.
117
197
  *
118
- * Rejects when the API is missing or the user denies; both are ordinary
119
- * outcomes and neither leaves the registry in a partial state, so a caller
120
- * that simply ignores the rejection still has a working SDF tier.
198
+ * Rejects when the API is missing or the user denies; neither leaves the
199
+ * registry in a partial state.
121
200
  */
122
201
  declare function enableLocalFontOutlines(opts?: LocalFontOutlinesOptions): Promise<LocalFontOutlinesResult>;
123
202
 
@@ -125,22 +204,14 @@ declare function enableLocalFontOutlines(opts?: LocalFontOutlinesOptions): Promi
125
204
  * The GLSL that turns an atlas sample into glyph coverage, and the constants
126
205
  * naming which kind of atlas a sample came from.
127
206
  *
128
- * Not a program: text has no program of its own any more. Glyphs stage into
129
- * the renderer's batch alongside solid geometry and image quads, and the batch
130
- * shader pastes this in — which is why what lives here is a snippet rather
131
- * than a vertex and fragment pair. The two channel layouts a glyph atlas can
132
- * have are the part that belongs to this package, so they stay here.
207
+ * Not a program: glyphs stage into the renderer's batch alongside solid
208
+ * geometry and image quads, and the batch shader pastes this in.
133
209
  *
134
- * MSDF channel layout: msdf-bmfont-xml writes R, G and B as independent
210
+ * MSDF channel layout: msdf-bmfont-xml writes R, G, and B as independent
135
211
  * signed-distance fields covering different edge directions, and the true
136
- * field is their median — which recovers a sharp outline while averaging out
137
- * single-channel aliasing. The runtime canvas bake writes one channel that
138
- * *is* the field. Both encode the edge at 0.5, which is what lets one
139
- * threshold serve them.
140
- *
141
- * The single-channel bake rounds corners away from its bake size, mildest near
142
- * it. `glyphRasterizer.ts` carries the measurements and the reason neither a
143
- * larger bake nor extra taps would improve the small-text end.
212
+ * field is their median. The runtime canvas bake writes one channel that *is*
213
+ * the field. Both encode the edge at 0.5, which is what lets one threshold
214
+ * serve them.
144
215
  */
145
216
 
146
217
  /** `a_paintMode` value for glyphs off an MSDF atlas — the median of R,G,B. */
@@ -151,30 +222,24 @@ declare const GLYPH_MODE_R8 = 2;
151
222
  * Glyph coverage from one atlas sample, as GLSL for a program to paste in.
152
223
  *
153
224
  * `mode` says which field the sample carries: `GLYPH_MODE_MSDF` takes the
154
- * median of R,G,B, `GLYPH_MODE_R8` reads `.r` alone. A caller whose fragment
155
- * is not a glyph at all still calls this and discards the result — see below.
225
+ * median of R,G,B, `GLYPH_MODE_R8` reads `.r` alone.
156
226
  *
157
- * The antialiasing band has to be one *screen* pixel wide, so it is measured
158
- * from how fast the atlas moves under the fragment: `fieldPerUv` is how far
159
- * the field runs per unit of texture coordinate on each axis (see
227
+ * The antialiasing band has to be one *screen* pixel wide: `fieldPerUv` is how
228
+ * far the field runs per unit of texture coordinate on each axis (see
160
229
  * `glyphFieldScale`), and the screen derivatives of `uv` turn that into field
161
- * units per screen pixel — one quantity folding in font size, zoom and DPR.
230
+ * units per screen pixel.
162
231
  *
163
- * Not `fwidth(field)`, which it was until 2026-09-29: that is the field's
164
- * slope only where the field is a straight ramp. Across a stem narrower than a
165
- * pixel — a 12px superscript's, minified 4x off the atlas — the two columns of
166
- * a 2x2 quad sample either side of the ridge at equal values, the derivative
167
- * reads flat, the band collapses to the floor, and the stem is thresholded
168
- * away. The derivative of `uv` is constant across a glyph quad, so it cannot
169
- * do that. Nor a constant band, which was 0.05 until 2026-07-29 and right at
170
- * one scale only. The `max()` floor keeps a degenerate derivative from
171
- * collapsing the band.
232
+ * Not `fwidth(field)`: that is the field's slope only where the field is a
233
+ * straight ramp. Across a stem narrower than a pixel the two columns of a 2x2
234
+ * quad sample either side of the ridge at equal values, the derivative reads
235
+ * flat, and the stem is thresholded away. The derivative of `uv` is constant
236
+ * across a glyph quad, so it cannot do that. Nor a constant band, which is
237
+ * right at one scale only.
172
238
  *
173
239
  * **Nothing here branches, and the caller must not branch around it.**
174
240
  * A derivative in non-uniform control flow is undefined, so it has to
175
- * be taken before anything selects on paint mode. That is why a merged program
176
- * runs the glyph math on fragments that are not glyphs, and it is why the two
177
- * fields are selected with a `mix` rather than an `if`.
241
+ * be taken before anything selects on paint mode. A merged program therefore
242
+ * runs the glyph math on fragments that are not glyphs and discards the result.
178
243
  *
179
244
  * `synthBold` shifts the threshold to thicken strokes where the resolver fell
180
245
  * back from a missing bold variant to the regular atlas.
@@ -189,4 +254,4 @@ declare const GLYPH_COVERAGE_GLSL = "\nfloat median(float r, float g, float b) {
189
254
  */
190
255
  declare function glyphFieldScale(source: 'atlas' | 'canvas', family: string, weight: number, style: FontStyle): readonly [number, number] | undefined;
191
256
 
192
- export { FontStyle, GLYPH_COVERAGE_GLSL, GLYPH_MODE_MSDF, GLYPH_MODE_R8, type LocalFontOutlinesOptions, type LocalFontOutlinesResult, canQueryLocalFonts, cssFamilyName, enableLocalFontOutlines, glyphFieldScale, glyphGeneration, parseFontStyle, subscribeGlyphReady };
257
+ export { BmFont, BmFontChar, DEFAULT_BAKE_BUDGET, FontStyle, GLYPH_COVERAGE_GLSL, GLYPH_MODE_MSDF, GLYPH_MODE_R8, type GlyphMetrics, type LocalFontOutlinesOptions, type LocalFontOutlinesResult, ResolveResult, type ResolvedGlyph, canQueryLocalFonts, cssFamilyName, enableLocalFontOutlines, faceGeneration, glyphFieldScale, glyphGeneration, glyphIn, metricsOf, parseFontStyle, resetBakeBudget, resolveColorCluster, resolveGlyph, subscribeGlyphReady, warnNoMetrics };
package/dist/index.js CHANGED
@@ -1,5 +1,150 @@
1
- import { registerFontOutlines, createOpenTypeParser, getFont, PAGE_SIZE, SDF_RADIUS } from './chunk-ERSHQYJL.js';
2
- export { DEFAULT_BAKE_BUDGET, FIXTURE_FONT, cssFamilyName, cssFontFamily, cssFontFamilyLoading, dynamicPageTextureId, ensureFontTexture, faceMetricsFor, faceMetricsFromTables, faceMetricsOf, fontOutlineRegistry, fontPending, fontRegistry, getDefaultFontFamily, getFont, getFontFallbackPolicy, glyphGeneration, glyphOutline, hasFontOutlines, isCanvasFont, listCanvasFonts, listFontOutlines, listFontWeights, listFonts, loadFontOutlines, markAllFontsNotUploaded, outlineMetrics, outlineStatus, parseBmFont, registerCanvasFont, registerFont, registerFontOutlines, resetBakeBudget, resolveFontVariant, resolveGlyphFallback, setDefaultFontFamily, setFontFallbackPolicy, subscribeGlyphReady, syncDynamicPageTexture, textureCacheKey, unregisterCanvasFont, unregisterFontOutlines, verticalMetricsFromTables, warmFonts } from './chunk-ERSHQYJL.js';
1
+ import { fontPending, fallbackWarningClaimed, claimFallbackWarning, resolveGlyphFallback, registerFontOutlines, createOpenTypeParser, getFont, PAGE_SIZE, SDF_RADIUS } from './chunk-NM6K6M4Y.js';
2
+ export { DEFAULT_BAKE_BUDGET, FIXTURE_FONT, colorPageTextureId, cssFamilyName, cssFontFamily, cssFontFamilyLoading, dynamicPageTextureId, ensureFontTexture, faceGeneration, faceMetricsFor, faceMetricsFromTables, faceMetricsOf, fontOutlineRegistry, fontPending, fontRegistry, getDefaultFontFamily, getFont, getFontFallbackPolicy, glyphGeneration, glyphOutline, hasFontOutlines, isCanvasFont, listCanvasFonts, listFontOutlines, listFontWeights, listFonts, loadFontOutlines, markAllFontsNotUploaded, outlineMetrics, outlineStatus, parseBmFont, refreshCanvasFont, registerCanvasFont, registerFont, registerFontOutlines, resetBakeBudget, resolveFontVariant, resolveGlyphFallback, setDefaultFontFamily, setFontFallbackPolicy, subscribeGlyphReady, syncColorPageTexture, syncDynamicPageTexture, textureCacheKey, unregisterCanvasFont, unregisterFontOutlines, verticalMetricsFromTables, warmFonts } from './chunk-NM6K6M4Y.js';
3
+
4
+ // src/defaultIgnorable.ts
5
+ var RANGES = [
6
+ [173, 173],
7
+ [847, 847],
8
+ [1564, 1564],
9
+ [4447, 4448],
10
+ [6068, 6069],
11
+ [6155, 6159],
12
+ [8203, 8207],
13
+ [8234, 8238],
14
+ [8288, 8303],
15
+ [12644, 12644],
16
+ [65024, 65039],
17
+ [65279, 65279],
18
+ [65440, 65440],
19
+ [65520, 65528],
20
+ [113824, 113827],
21
+ [119155, 119162],
22
+ [917504, 921599]
23
+ ];
24
+ function isDefaultIgnorable(cp) {
25
+ return RANGES.some(([lo, hi]) => cp >= lo && cp <= hi);
26
+ }
27
+
28
+ // src/glyphMetrics.ts
29
+ function usableGlyph(glyph) {
30
+ return glyph.xadvance > 0 || glyph.width > 0 || glyph.page < 0 || isDefaultIgnorable(glyph.id);
31
+ }
32
+ function atlasMetrics(font, advanceOf) {
33
+ const stated = () => {
34
+ const face = font.faceMetrics;
35
+ return face?.ascent !== void 0 && face.descent !== void 0 ? face : void 0;
36
+ };
37
+ return {
38
+ size: font.info.size,
39
+ get base() {
40
+ return font.common.base;
41
+ },
42
+ get ascent() {
43
+ const face = stated();
44
+ return face ? face.ascent * font.info.size : font.common.base;
45
+ },
46
+ get descent() {
47
+ const face = stated();
48
+ return face && face.descent * font.info.size;
49
+ },
50
+ advanceOf,
51
+ kernOf: (l, r) => font.kerningMap.get(l)?.get(r) ?? 0
52
+ };
53
+ }
54
+ function outlineMetricsOf(face) {
55
+ const descent = face.faceMetrics?.descent;
56
+ return {
57
+ size: 1,
58
+ base: face.ascender,
59
+ ascent: face.faceMetrics?.ascent ?? face.ascender,
60
+ ...descent !== void 0 ? { descent } : {},
61
+ advanceOf: (cp) => face.advanceOf(cp),
62
+ kernOf: (l, r) => face.kernOf(l, r)
63
+ };
64
+ }
65
+ function canvasMetrics(face) {
66
+ return atlasMetrics(face.font, (cp) => {
67
+ const glyph = face.requestGlyph(cp);
68
+ return usableGlyph(glyph) ? glyph.xadvance : null;
69
+ });
70
+ }
71
+ var byFace = /* @__PURE__ */ new WeakMap();
72
+ function memo(face, build) {
73
+ let m = byFace.get(face);
74
+ if (!m) {
75
+ m = build(face);
76
+ byFace.set(face, m);
77
+ }
78
+ return m;
79
+ }
80
+ function metricsOf(result) {
81
+ if (result.outlineFace) return memo(result.outlineFace, outlineMetricsOf);
82
+ if (result.entry) {
83
+ return memo(result.entry.font, (font) => atlasMetrics(font, (cp) => font.charMap.get(cp)?.xadvance ?? null));
84
+ }
85
+ if (result.dynamicFace) return memo(result.dynamicFace, canvasMetrics);
86
+ return void 0;
87
+ }
88
+
89
+ // src/emojiCluster.ts
90
+ var EMOJI = /\p{Emoji_Presentation}|\p{Extended_Pictographic}\uFE0F|\uFE0F\u20E3/u;
91
+ var FIRST_DEFAULT_EMOJI = 8986;
92
+ function hasEmojiPresentation(cluster) {
93
+ if (cluster.length === 1 && cluster.charCodeAt(0) < FIRST_DEFAULT_EMOJI) return false;
94
+ return EMOJI.test(cluster) && !cluster.includes("\uFE0E");
95
+ }
96
+
97
+ // src/glyphWarnings.ts
98
+ function warnMissingGlyph(family, cp) {
99
+ if (!claimFallbackWarning(`missing-glyph|${family}|${cp}`)) return;
100
+ const ch = String.fromCodePoint(cp);
101
+ console.warn(
102
+ `weasel: no glyph for U+${cp.toString(16).toUpperCase().padStart(4, "0")} (${JSON.stringify(ch)}) in "${family}", and the dynamic tier could not rasterize it \u2014 skipping the character. Bake it into the atlas, or call registerCanvasFont("${family}") to serve missing codepoints from installed fonts.`
103
+ );
104
+ }
105
+ var NOTHING_RENDERS = ["unusable-default", "self-unusable-default", "unregistered-default"];
106
+ function warnNoMetrics(family, weight, style) {
107
+ if (fontPending(family, weight, style)) return;
108
+ const face = `${family}|${weight}|${style}`;
109
+ if (NOTHING_RENDERS.some((kind) => fallbackWarningClaimed(`${kind}|${face}`))) return;
110
+ if (!claimFallbackWarning(`no-metrics|${face}`)) return;
111
+ console.warn(
112
+ `weasel: no metrics for "${family}" ${weight}/${style} \u2014 neither an atlas nor an outline face resolved, so every run set in it lays out as nothing. Register it with registerFont / registerCanvasFont / registerFontOutlines. If you did, check for a duplicated @weasel-js/font in node_modules \u2014 its registry is module state, so a second copy is a second, empty one.`
113
+ );
114
+ }
115
+
116
+ // src/resolveGlyph.ts
117
+ function glyphIn(resolved, cp) {
118
+ const metrics = metricsOf(resolved);
119
+ if (!metrics) return null;
120
+ if (resolved.outlineFace) {
121
+ const advance = metrics.advanceOf(cp);
122
+ return advance === null ? null : { resolved, metrics, advance, font: null, glyph: null };
123
+ }
124
+ const face = resolved.dynamicFace;
125
+ const font = resolved.entry?.font ?? face?.font;
126
+ const glyph = resolved.entry ? font?.charMap.get(cp) : face?.requestGlyph(cp);
127
+ if (!font || !glyph || face && !usableGlyph(glyph)) return null;
128
+ return { resolved, metrics, advance: glyph.xadvance, font, glyph };
129
+ }
130
+ function resolveGlyph(resolved, family, weight, style, cp, warn = true) {
131
+ const own = glyphIn(resolved, cp);
132
+ if (own) return own;
133
+ const fallback = resolveGlyphFallback(family, weight, style);
134
+ const escalated = fallback && glyphIn(fallback, cp);
135
+ if (escalated) return escalated;
136
+ if (warn) warnMissingGlyph(resolved.resolved.family, cp);
137
+ return null;
138
+ }
139
+ function resolveColorCluster(resolved, family, weight, style, cluster) {
140
+ if (!hasEmojiPresentation(cluster)) return null;
141
+ const tier = resolved.dynamicFace ? resolved : resolveGlyphFallback(family, weight, style);
142
+ const face = tier?.dynamicFace;
143
+ const metrics = tier && metricsOf(tier);
144
+ const glyph = face?.requestCluster(cluster);
145
+ if (!tier || !face || !metrics || !glyph) return null;
146
+ return { resolved: tier, metrics, advance: glyph.xadvance, font: face.font, glyph };
147
+ }
3
148
 
4
149
  // src/outline/localFonts.ts
5
150
  function canQueryLocalFonts() {
@@ -105,6 +250,6 @@ function glyphFieldScale(source, family, weight, style) {
105
250
  return [font.common.scaleW / range, font.common.scaleH / range];
106
251
  }
107
252
 
108
- export { GLYPH_COVERAGE_GLSL, GLYPH_MODE_MSDF, GLYPH_MODE_R8, canQueryLocalFonts, enableLocalFontOutlines, glyphFieldScale, parseFontStyle };
253
+ export { GLYPH_COVERAGE_GLSL, GLYPH_MODE_MSDF, GLYPH_MODE_R8, canQueryLocalFonts, enableLocalFontOutlines, glyphFieldScale, glyphIn, metricsOf, parseFontStyle, resolveColorCluster, resolveGlyph, warnNoMetrics };
109
254
  //# sourceMappingURL=index.js.map
110
255
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/outline/localFonts.ts","../src/textSdf.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Local Font Access → outline registrations.\n *\n * The dynamic canvas-SDF tier can render any installed family without ever\n * seeing its bytes: it asks canvas 2D to `fillText` and takes the pixels. The\n * outline tier cannot — it needs the actual font file — and the only web API\n * that hands one over is `window.queryLocalFonts()`, which is Chromium-only\n * and gated behind a permission prompt that requires a user gesture.\n *\n * So this is the one part of the tier a consumer has to *ask* for, from a\n * click. Everything downstream treats a refusal as ordinary: no\n * registrations, `glyphOutline` keeps answering `null`, and large text keeps\n * rendering from the SDF tier exactly as it does today. That ladder is\n * load-bearing rather than defensive — Safari and Firefox have no equivalent\n * API at all, so \"denied\" is the permanent state for most of the web.\n *\n * Registration is eager but reading is lazy: every matching face gets a thunk\n * that calls `FontData.blob()` at first glyph request. A machine can easily\n * have hundreds of faces and a `.ttc` runs to tens of megabytes, so\n * downloading them all at enable time would be worse than not offering the\n * tier.\n */\n\nimport type { FontStyle } from '../fontStyle';\nimport { createOpenTypeParser } from './opentypeParser';\nimport { registerFontOutlines } from './outlineRegistry';\n\n/**\n * The slice of the Local Font Access API this module uses. Declared here\n * rather than pulled from a lib: `queryLocalFonts` is not in TypeScript's DOM\n * types, and the alternative — casting `window` to `any` at the call site —\n * would lose the shape of what comes back.\n */\ninterface LocalFontData {\n postscriptName: string;\n fullName: string;\n family: string;\n style: string;\n blob(): Promise<Blob>;\n}\n\ntype LocalFontWindow = typeof globalThis & {\n queryLocalFonts?: (opts?: { postscriptNames?: string[] }) => Promise<LocalFontData[]>;\n};\n\n/** Is the Local Font Access API present at all? False on every non-Chromium\n * browser and in any non-secure context. */\nexport function canQueryLocalFonts(): boolean {\n return typeof globalThis !== 'undefined'\n && typeof (globalThis as LocalFontWindow).queryLocalFonts === 'function';\n}\n\n/**\n * CSS weights for the weight words that appear in a `style` string.\n *\n * Ordered longest-first where one name contains another (\"ExtraBold\" before\n * \"Bold\", \"DemiBold\" before \"Bold\"), because the match is a substring scan\n * and \"Bold\" would otherwise claim every heavier face for 700.\n */\nconst WEIGHT_WORDS: readonly (readonly [string, number])[] = [\n ['extrablack', 950], ['ultrablack', 950],\n ['extrabold', 800], ['ultrabold', 800],\n ['extralight', 200], ['ultralight', 200],\n ['semibold', 600], ['demibold', 600],\n ['semilight', 350],\n ['black', 900], ['heavy', 900],\n ['bold', 700],\n ['medium', 500],\n ['light', 300],\n ['thin', 100], ['hairline', 100],\n ['book', 400], ['regular', 400], ['normal', 400],\n];\n\n/** Lowercased, with a split weight name closed up: \"Demi Bold\" and\n * \"Semi-Bold\" read as the single words the table above lists. */\nfunction joinWeightWords(style: string): string {\n return style.toLowerCase().replace(/\\b(extra|ultra|semi|demi)[\\s_-]+/g, '$1');\n}\n\n/**\n * Parse a `FontData.style` string — \"Regular\", \"Bold Italic\", \"Condensed\n * ExtraLight Oblique\" — into the CSS variant the kit's registry is keyed by.\n *\n * Anything unrecognized lands on 400/normal, which is what the string\n * \"Regular\" would have produced anyway. A wrong guess here costs a face that\n * registers under a variant nothing asks for; it cannot mis-paint anything,\n * because resolution is an exact-match lookup that simply misses.\n */\nexport function parseFontStyle(style: string): { weight: number; style: FontStyle } {\n const s = joinWeightWords(style);\n const italic = s.includes('italic') || s.includes('oblique');\n const weight = WEIGHT_WORDS.find(([word]) => s.includes(word))?.[1] ?? 400;\n return { weight, style: italic ? 'italic' : 'normal' };\n}\n\n/**\n * How many words of a `FontData.style` string are *not* weight or slant —\n * \"Condensed\", \"Display\", \"Poster\". A face with none of them is the plain\n * member of its slot, and the one to keep when two faces collide.\n */\nfunction styleQualifierCount(style: string): number {\n const words = joinWeightWords(style).split(/[\\s_-]+/).filter(Boolean);\n return words.filter((w) => w !== 'italic' && w !== 'oblique'\n && !WEIGHT_WORDS.some(([word]) => word === w)).length;\n}\n\n/** Options for `registerLocalFontOutlines`. */\nexport interface LocalFontOutlinesOptions {\n /**\n * Restrict registration to these families. Defaults to every installed\n * family. Worth passing when the consumer already knows its font menu:\n * a machine with 900 faces otherwise gets 900 registry entries, all inert\n * but all held.\n */\n families?: readonly string[];\n}\n\n/** What `registerLocalFontOutlines` registered. */\nexport interface LocalFontOutlinesResult {\n /** Families that now have at least one outline face, in menu order. */\n families: readonly string[];\n /** Total faces registered across those families. */\n faces: number;\n}\n\n/**\n * Ask for permission to read installed fonts, and register outline sources\n * for the faces it returns. **Must be called from a user gesture** — the\n * permission prompt requires one, and without it the call rejects.\n *\n * Rejects when the API is missing or the user denies; both are ordinary\n * outcomes and neither leaves the registry in a partial state, so a caller\n * that simply ignores the rejection still has a working SDF tier.\n */\nexport async function enableLocalFontOutlines(\n opts: LocalFontOutlinesOptions = {},\n): Promise<LocalFontOutlinesResult> {\n const query = (globalThis as LocalFontWindow).queryLocalFonts;\n if (typeof query !== 'function') {\n throw new Error(\n 'weasel enableLocalFontOutlines: this browser has no Local Font Access API ' +\n '(queryLocalFonts). Large text keeps rendering from the SDF tier.',\n );\n }\n\n const wanted = opts.families ? new Set(opts.families) : null;\n const data = await query.call(globalThis);\n\n // Several installed faces can land on one (family, weight, style) slot —\n // \"Helvetica Neue Condensed Bold\" reduces to 700/normal exactly like\n // \"Helvetica Neue Bold\". Registering both left whichever `queryLocalFonts`\n // happened to return last, so a condensed face could paint at the upright\n // face's advances. Pick the least-qualified name for each slot instead.\n const chosen = new Map<string, { font: LocalFontData; qualifiers: number }>();\n for (const font of data) {\n if (wanted && !wanted.has(font.family)) continue;\n const { weight, style } = parseFontStyle(font.style);\n const key = `${font.family}|${weight}|${style}`;\n const qualifiers = styleQualifierCount(font.style);\n const prev = chosen.get(key);\n if (prev && prev.qualifiers <= qualifiers) continue;\n chosen.set(key, { font, qualifiers });\n }\n\n const families = new Set<string>();\n for (const { font } of chosen.values()) {\n registerFontOutlines(font.family, parseFontStyle(font.style), () => font.blob(), {\n // The PostScript name is how a member is picked out of a `.ttc`\n // collection, which is what most macOS system families ship as.\n parser: createOpenTypeParser(font.postscriptName),\n // The DOM already has every installed face; naming it avoids reading\n // the collection file a second time.\n cssSrc: `local(${JSON.stringify(font.postscriptName)})`,\n });\n families.add(font.family);\n }\n return {\n families: [...families].sort((a, b) => a.localeCompare(b)),\n faces: chosen.size,\n };\n}\n","/**\n * The GLSL that turns an atlas sample into glyph coverage, and the constants\n * naming which kind of atlas a sample came from.\n *\n * Not a program: text has no program of its own any more. Glyphs stage into\n * the renderer's batch alongside solid geometry and image quads, and the batch\n * shader pastes this in — which is why what lives here is a snippet rather\n * than a vertex and fragment pair. The two channel layouts a glyph atlas can\n * have are the part that belongs to this package, so they stay here.\n *\n * MSDF channel layout: msdf-bmfont-xml writes R, G and B as independent\n * signed-distance fields covering different edge directions, and the true\n * field is their median — which recovers a sharp outline while averaging out\n * single-channel aliasing. The runtime canvas bake writes one channel that\n * *is* the field. Both encode the edge at 0.5, which is what lets one\n * threshold serve them.\n *\n * The single-channel bake rounds corners away from its bake size, mildest near\n * it. `glyphRasterizer.ts` carries the measurements and the reason neither a\n * larger bake nor extra taps would improve the small-text end.\n */\n\nimport type { FontStyle } from './fontStyle';\nimport { getFont } from './registerFont';\nimport { PAGE_SIZE, SDF_RADIUS } from './dynamic/dynamicAtlas';\n\n/** `a_paintMode` value for glyphs off an MSDF atlas — the median of R,G,B. */\nexport const GLYPH_MODE_MSDF = 1;\n/** `a_paintMode` value for glyphs off a runtime canvas bake — `.r` alone. */\nexport const GLYPH_MODE_R8 = 2;\n\n/**\n * Glyph coverage from one atlas sample, as GLSL for a program to paste in.\n *\n * `mode` says which field the sample carries: `GLYPH_MODE_MSDF` takes the\n * median of R,G,B, `GLYPH_MODE_R8` reads `.r` alone. A caller whose fragment\n * is not a glyph at all still calls this and discards the result — see below.\n *\n * The antialiasing band has to be one *screen* pixel wide, so it is measured\n * from how fast the atlas moves under the fragment: `fieldPerUv` is how far\n * the field runs per unit of texture coordinate on each axis (see\n * `glyphFieldScale`), and the screen derivatives of `uv` turn that into field\n * units per screen pixel — one quantity folding in font size, zoom and DPR.\n *\n * Not `fwidth(field)`, which it was until 2026-09-29: that is the field's\n * slope only where the field is a straight ramp. Across a stem narrower than a\n * pixel — a 12px superscript's, minified 4x off the atlas — the two columns of\n * a 2x2 quad sample either side of the ridge at equal values, the derivative\n * reads flat, the band collapses to the floor, and the stem is thresholded\n * away. The derivative of `uv` is constant across a glyph quad, so it cannot\n * do that. Nor a constant band, which was 0.05 until 2026-07-29 and right at\n * one scale only. The `max()` floor keeps a degenerate derivative from\n * collapsing the band.\n *\n * **Nothing here branches, and the caller must not branch around it.**\n * A derivative in non-uniform control flow is undefined, so it has to\n * be taken before anything selects on paint mode. That is why a merged program\n * runs the glyph math on fragments that are not glyphs, and it is why the two\n * fields are selected with a `mix` rather than an `if`.\n *\n * `synthBold` shifts the threshold to thicken strokes where the resolver fell\n * back from a missing bold variant to the regular atlas.\n */\nexport const GLYPH_COVERAGE_GLSL = /* glsl */ `\nfloat median(float r, float g, float b) {\n return max(min(r, g), min(max(r, g), b));\n}\n\nfloat glyphCoverage(vec4 texel, float mode, float synthBold, vec2 uv, vec2 fieldPerUv) {\n float field = mix(median(texel.r, texel.g, texel.b), texel.r, step(1.5, mode));\n float fieldPerPx = 0.5 * (length(dFdx(uv) * fieldPerUv) + length(dFdy(uv) * fieldPerUv));\n float aaW = max(0.5 * fieldPerPx, 0.0005);\n float threshold = 0.5 - synthBold;\n return smoothstep(threshold - aaW, threshold + aaW, field);\n}\n`;\n\n/** msdf-bmfont-xml's default `distanceRange`, for an atlas that records none. */\nconst DEFAULT_MSDF_RANGE = 4;\n\n/** A runtime canvas page: `PAGE_SIZE` texels, field 0 to 1 over `SDF_RADIUS` of them. */\nconst CANVAS_FIELD_SCALE = [PAGE_SIZE / SDF_RADIUS, PAGE_SIZE / SDF_RADIUS] as const;\n\n/**\n * Field units per unit of texture coordinate along u and v for the atlas a\n * glyph group samples — the `fieldPerUv` that `glyphCoverage` takes. A baked\n * MSDF atlas answers from its page size and `distanceRange`, a runtime canvas\n * page from the dynamic tier's page size and bake radius. `undefined` for an\n * atlas that is not registered.\n */\nexport function glyphFieldScale(\n source: 'atlas' | 'canvas', family: string, weight: number, style: FontStyle,\n): readonly [number, number] | undefined {\n if (source === 'canvas') return CANVAS_FIELD_SCALE;\n const font = getFont(family, weight, style)?.font;\n if (!font) return undefined;\n const range = font.distanceRange ?? DEFAULT_MSDF_RANGE;\n return [font.common.scaleW / range, font.common.scaleH / range];\n}\n"]}
1
+ {"version":3,"sources":["../src/defaultIgnorable.ts","../src/glyphMetrics.ts","../src/emojiCluster.ts","../src/glyphWarnings.ts","../src/resolveGlyph.ts","../src/outline/localFonts.ts","../src/textSdf.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/** Unicode's `Default_Ignorable_Code_Point` ranges (DerivedCoreProperties.txt). */\nconst RANGES: ReadonlyArray<readonly [number, number]> = [\n [0x00ad, 0x00ad], [0x034f, 0x034f], [0x061c, 0x061c], [0x115f, 0x1160],\n [0x17b4, 0x17b5], [0x180b, 0x180f], [0x200b, 0x200f], [0x202a, 0x202e],\n [0x2060, 0x206f], [0x3164, 0x3164], [0xfe00, 0xfe0f], [0xfeff, 0xfeff],\n [0xffa0, 0xffa0], [0xfff0, 0xfff8], [0x1bca0, 0x1bca3], [0x1d173, 0x1d17a],\n [0xe0000, 0xe0fff],\n];\n\n/**\n * Whether `cp` is one a renderer shows as nothing by design: joiners,\n * variation selectors, zero-width spaces, bidi controls, the soft hyphen. A\n * face serving one as no advance and no ink has served it.\n */\nexport function isDefaultIgnorable(cp: number): boolean {\n return RANGES.some(([lo, hi]) => cp >= lo && cp <= hi);\n}\n","/**\n * One shape for the advances, kerning and baseline of whichever tier resolved\n * a face. A baked atlas and the canvas tier measure in their bake-size pixels\n * and a parsed face in ems; `size` is the divisor that takes any of them to\n * world units (`fontSize / size`), which is why an em-space face reports 1.\n */\nimport type { BmFont, BmFontChar } from './FontAtlas';\nimport { isDefaultIgnorable } from './defaultIgnorable';\nimport type { DynamicFace } from './dynamic/dynamicAtlas';\nimport type { OutlineFace } from './outline/OutlineFace';\nimport type { ResolveResult } from './registerFont';\n\n/** Where a face's advances, kerning and baseline come from. */\nexport interface GlyphMetrics {\n /** Units per em of everything else here. */\n readonly size: number;\n /** Line top to baseline in the glyph frame: what an atlas glyph's `yoffset`\n * is measured against. */\n readonly base: number;\n /** Baseline to the top of the face's line box: the ascent a browser sets\n * the face with, or `base` for a face that states none. */\n readonly ascent: number;\n /** Baseline to the bottom of the face's line box. Absent for a face that\n * states no ascent and descent, which hangs its baseline `base` below the\n * line top instead of centering the face in its line. */\n readonly descent?: number;\n /** Advance for `cp`, or null when the face has no glyph for it. On the\n * canvas tier this measures the glyph and queues its bake. */\n advanceOf(cp: number): number | null;\n kernOf(left: number, right: number): number;\n}\n\n/** A canvas face that cannot measure a code point reports no advance and no\n * ink for it, which is its only way to say it has no glyph, and exactly how\n * it serves a default-ignorable one. A glyph with ink has no width until its\n * bake lands, and says so with a negative page. */\nexport function usableGlyph(glyph: BmFontChar): boolean {\n return glyph.xadvance > 0 || glyph.width > 0 || glyph.page < 0 || isDefaultIgnorable(glyph.id);\n}\n\n/** Reads through to `font` rather than copying: the canvas tier measures a\n * face again in place when its web font lands. */\nfunction atlasMetrics(font: BmFont, advanceOf: (cp: number) => number | null): GlyphMetrics {\n const stated = (): { ascent: number; descent: number } | undefined => {\n const face = font.faceMetrics;\n return face?.ascent !== undefined && face.descent !== undefined\n ? face as { ascent: number; descent: number }\n : undefined;\n };\n return {\n size: font.info.size,\n get base() { return font.common.base; },\n get ascent() {\n const face = stated();\n return face ? face.ascent * font.info.size : font.common.base;\n },\n get descent() {\n const face = stated();\n return face && face.descent * font.info.size;\n },\n advanceOf,\n kernOf: (l, r) => font.kerningMap.get(l)?.get(r) ?? 0,\n };\n}\n\nfunction outlineMetricsOf(face: OutlineFace): GlyphMetrics {\n const descent = face.faceMetrics?.descent;\n return {\n size: 1,\n base: face.ascender,\n ascent: face.faceMetrics?.ascent ?? face.ascender,\n ...(descent !== undefined ? { descent } : {}),\n advanceOf: (cp) => face.advanceOf(cp),\n kernOf: (l, r) => face.kernOf(l, r),\n };\n}\n\nfunction canvasMetrics(face: DynamicFace): GlyphMetrics {\n return atlasMetrics(face.font, (cp) => {\n const glyph = face.requestGlyph(cp);\n return usableGlyph(glyph) ? glyph.xadvance : null;\n });\n}\n\nconst byFace = new WeakMap<object, GlyphMetrics>();\n\nfunction memo<F extends object>(face: F, build: (face: F) => GlyphMetrics): GlyphMetrics {\n let m = byFace.get(face);\n if (!m) {\n m = build(face);\n byFace.set(face, m);\n }\n return m;\n}\n\n/**\n * The metrics of whichever tier `result` resolved to, or `undefined` for a\n * miss, which has none. One object per face, so two results that landed on the\n * same face compare equal by identity.\n */\nexport function metricsOf(result: ResolveResult): GlyphMetrics | undefined {\n if (result.outlineFace) return memo(result.outlineFace, outlineMetricsOf);\n if (result.entry) {\n return memo(result.entry.font, (font) => atlasMetrics(font, (cp) => font.charMap.get(cp)?.xadvance ?? null));\n }\n if (result.dynamicFace) return memo(result.dynamicFace, canvasMetrics);\n return undefined;\n}\n","const EMOJI = /\\p{Emoji_Presentation}|\\p{Extended_Pictographic}\\uFE0F|\\uFE0F\\u20E3/u;\n\n/** The first code point that is an emoji by default (U+231A WATCH). */\nconst FIRST_DEFAULT_EMOJI = 0x231a;\n\n/**\n * Whether a grapheme cluster is drawn as an emoji rather than as text: it\n * holds a code point that is one by default, or one made so by U+FE0F, and no\n * U+FE0E asking for text.\n */\nexport function hasEmojiPresentation(cluster: string): boolean {\n if (cluster.length === 1 && cluster.charCodeAt(0) < FIRST_DEFAULT_EMOJI) return false;\n return EMOJI.test(cluster) && !cluster.includes('\\uFE0E');\n}\n","/**\n * Warnings about what the registered fonts cannot serve. Text is laid out per\n * frame, so each is claimed once through the fallback warning gate, which the\n * registry and fallback resets both clear.\n */\nimport { claimFallbackWarning, fallbackWarningClaimed } from './fallback';\nimport type { FontStyle } from './fontStyle';\nimport { fontPending } from './registerFont';\n\n/** Say, once per family and code point, that no tier has a glyph for it. */\nexport function warnMissingGlyph(family: string, cp: number): void {\n if (!claimFallbackWarning(`missing-glyph|${family}|${cp}`)) return;\n const ch = String.fromCodePoint(cp);\n console.warn(\n `weasel: no glyph for U+${cp.toString(16).toUpperCase().padStart(4, '0')} ` +\n `(${JSON.stringify(ch)}) in \"${family}\", and the dynamic tier could not ` +\n `rasterize it — skipping the character. Bake it into the atlas, or call ` +\n `registerCanvasFont(\"${family}\") to serve missing codepoints from ` +\n `installed fonts.`,\n );\n}\n\n/** The registry's own warnings that a face will draw nothing, by key prefix. */\nconst NOTHING_RENDERS = ['unusable-default', 'self-unusable-default', 'unregistered-default'];\n\n/**\n * Say, once per face, that text set in it has no metrics and lays out as\n * nothing. Silent while the face is still loading, which is late rather than\n * missing, and when the registry has already said why it cannot render.\n */\nexport function warnNoMetrics(family: string, weight: number, style: FontStyle): void {\n if (fontPending(family, weight, style)) return;\n const face = `${family}|${weight}|${style}`;\n if (NOTHING_RENDERS.some((kind) => fallbackWarningClaimed(`${kind}|${face}`))) return;\n if (!claimFallbackWarning(`no-metrics|${face}`)) return;\n console.warn(\n `weasel: no metrics for \"${family}\" ${weight}/${style} — neither ` +\n `an atlas nor an outline face resolved, so every run set in it lays out as ` +\n `nothing. Register it with registerFont / registerCanvasFont / ` +\n `registerFontOutlines. If you did, check for a duplicated @weasel-js/font in ` +\n `node_modules — its registry is module state, so a second copy is a second, ` +\n `empty one.`,\n );\n}\n","/**\n * Finding the glyph for one code point: in the tier a run resolved to, then\n * in the dynamic tier, then nowhere. A miss never draws a stand-in `?`, which\n * is indistinguishable from one the author wrote, and the BmFont atlas format\n * has no `.notdef` to draw.\n */\nimport type { BmFont, BmFontChar } from './FontAtlas';\nimport { hasEmojiPresentation } from './emojiCluster';\nimport type { FontStyle } from './fontStyle';\nimport { metricsOf, usableGlyph, type GlyphMetrics } from './glyphMetrics';\nimport { warnMissingGlyph } from './glyphWarnings';\nimport { resolveGlyphFallback, type ResolveResult } from './registerFont';\n\n/** A code point's glyph, and the tier that ended up serving it. */\nexport interface ResolvedGlyph {\n /** The tier serving the glyph. Not the run's own when the code point\n * escalated to the dynamic tier. */\n resolved: ResolveResult;\n /** That tier's metrics, which are what `advance` is measured in. */\n metrics: GlyphMetrics;\n advance: number;\n /** The atlas to sample and the glyph's record in it. Both null on the\n * outline tier, which has geometry instead. A canvas glyph's record gains\n * its atlas rect in place when its bake lands. */\n font: BmFont | null;\n glyph: BmFontChar | null;\n}\n\n/** The glyph for `cp` in the tier `resolved` landed on, or null when that\n * tier has none. */\nexport function glyphIn(resolved: ResolveResult, cp: number): ResolvedGlyph | null {\n const metrics = metricsOf(resolved);\n if (!metrics) return null;\n if (resolved.outlineFace) {\n const advance = metrics.advanceOf(cp);\n return advance === null ? null : { resolved, metrics, advance, font: null, glyph: null };\n }\n const face = resolved.dynamicFace;\n const font = resolved.entry?.font ?? face?.font;\n const glyph = resolved.entry ? font?.charMap.get(cp) : face?.requestGlyph(cp);\n if (!font || !glyph || (face && !usableGlyph(glyph))) return null;\n return { resolved, metrics, advance: glyph.xadvance, font, glyph };\n}\n\n/**\n * The glyph for `cp` in a run that asked for `(family, weight, style)` and\n * resolved to `resolved`: that tier's own, else the dynamic tier's under the\n * face the run asked for. Null when neither has it, with a warning naming the\n * code point unless `warn` is false.\n */\nexport function resolveGlyph(\n resolved: ResolveResult,\n family: string,\n weight: number,\n style: FontStyle,\n cp: number,\n warn = true,\n): ResolvedGlyph | null {\n const own = glyphIn(resolved, cp);\n if (own) return own;\n const fallback = resolveGlyphFallback(family, weight, style);\n const escalated = fallback && glyphIn(fallback, cp);\n if (escalated) return escalated;\n if (warn) warnMissingGlyph(resolved.resolved.family, cp);\n return null;\n}\n\n/**\n * The color glyph for a whole grapheme cluster with emoji presentation, in a\n * run that asked for `(family, weight, style)` and resolved to `resolved`.\n * The canvas tier is the only one that draws a cluster as one glyph or in its\n * own colors, so it serves these whatever tier serves the run's text. Null for\n * any other cluster, and where the canvas tier is not to be had or draws no\n * color for this one: the cluster is then laid out code point by code point.\n */\nexport function resolveColorCluster(\n resolved: ResolveResult,\n family: string,\n weight: number,\n style: FontStyle,\n cluster: string,\n): ResolvedGlyph | null {\n if (!hasEmojiPresentation(cluster)) return null;\n const tier = resolved.dynamicFace ? resolved : resolveGlyphFallback(family, weight, style);\n const face = tier?.dynamicFace;\n const metrics = tier && metricsOf(tier);\n const glyph = face?.requestCluster(cluster);\n if (!tier || !face || !metrics || !glyph) return null;\n return { resolved: tier, metrics, advance: glyph.xadvance, font: face.font, glyph };\n}\n","/**\n * Local Font Access → outline registrations.\n *\n * The dynamic canvas-SDF tier can render any installed family without ever\n * seeing its bytes: it asks canvas 2D to `fillText` and takes the pixels. The\n * outline tier cannot — it needs the actual font file — and the only web API\n * that hands one over is `window.queryLocalFonts()`, which is Chromium-only\n * and gated behind a permission prompt that requires a user gesture.\n *\n * So this is the one part of the tier a consumer has to *ask* for, from a\n * click. A refusal is ordinary: no registrations, and large text keeps\n * rendering from the SDF tier. Safari and Firefox have no equivalent API at\n * all, so \"denied\" is the permanent state for most of the web.\n *\n * Registration is eager but reading is lazy: every matching face gets a thunk\n * that calls `FontData.blob()` at first glyph request. A machine can have\n * hundreds of faces and a `.ttc` runs to tens of megabytes.\n */\n\nimport type { FontStyle } from '../fontStyle';\nimport { createOpenTypeParser } from './opentypeParser';\nimport { registerFontOutlines } from './outlineRegistry';\n\n/**\n * The slice of the Local Font Access API this module uses. `queryLocalFonts`\n * is not in TypeScript's DOM types.\n */\ninterface LocalFontData {\n postscriptName: string;\n fullName: string;\n family: string;\n style: string;\n blob(): Promise<Blob>;\n}\n\ntype LocalFontWindow = typeof globalThis & {\n queryLocalFonts?: (opts?: { postscriptNames?: string[] }) => Promise<LocalFontData[]>;\n};\n\n/** Is the Local Font Access API present at all? False on every non-Chromium\n * browser and in any non-secure context. */\nexport function canQueryLocalFonts(): boolean {\n return typeof globalThis !== 'undefined'\n && typeof (globalThis as LocalFontWindow).queryLocalFonts === 'function';\n}\n\n/**\n * CSS weights for the weight words that appear in a `style` string.\n *\n * Ordered longest-first where one name contains another (\"ExtraBold\" before\n * \"Bold\", \"DemiBold\" before \"Bold\"), because the match is a substring scan\n * and \"Bold\" would otherwise claim every heavier face for 700.\n */\nconst WEIGHT_WORDS: readonly (readonly [string, number])[] = [\n ['extrablack', 950], ['ultrablack', 950],\n ['extrabold', 800], ['ultrabold', 800],\n ['extralight', 200], ['ultralight', 200],\n ['semibold', 600], ['demibold', 600],\n ['semilight', 350],\n ['black', 900], ['heavy', 900],\n ['bold', 700],\n ['medium', 500],\n ['light', 300],\n ['thin', 100], ['hairline', 100],\n ['book', 400], ['regular', 400], ['normal', 400],\n];\n\n/** Lowercased, with a split weight name closed up: \"Demi Bold\" and\n * \"Semi-Bold\" read as the single words the table above lists. */\nfunction joinWeightWords(style: string): string {\n return style.toLowerCase().replace(/\\b(extra|ultra|semi|demi)[\\s_-]+/g, '$1');\n}\n\n/**\n * Parse a `FontData.style` string — \"Regular\", \"Bold Italic\", \"Condensed\n * ExtraLight Oblique\" — into the CSS variant the kit's registry is keyed by.\n *\n * Anything unrecognized lands on 400/normal.\n */\nexport function parseFontStyle(style: string): { weight: number; style: FontStyle } {\n const s = joinWeightWords(style);\n const italic = s.includes('italic') || s.includes('oblique');\n const weight = WEIGHT_WORDS.find(([word]) => s.includes(word))?.[1] ?? 400;\n return { weight, style: italic ? 'italic' : 'normal' };\n}\n\n/**\n * How many words of a `FontData.style` string are *not* weight or slant —\n * \"Condensed\", \"Display\", \"Poster\". A face with none of them is the plain\n * member of its slot, and the one to keep when two faces collide.\n */\nfunction styleQualifierCount(style: string): number {\n const words = joinWeightWords(style).split(/[\\s_-]+/).filter(Boolean);\n return words.filter((w) => w !== 'italic' && w !== 'oblique'\n && !WEIGHT_WORDS.some(([word]) => word === w)).length;\n}\n\n/** Options for `enableLocalFontOutlines`. */\nexport interface LocalFontOutlinesOptions {\n /**\n * Restrict registration to these families. Defaults to every installed\n * family, which can be hundreds of registry entries.\n */\n families?: readonly string[];\n}\n\n/** What `enableLocalFontOutlines` registered. */\nexport interface LocalFontOutlinesResult {\n /** Families that now have at least one outline face, in menu order. */\n families: readonly string[];\n /** Total faces registered across those families. */\n faces: number;\n}\n\n/**\n * Ask for permission to read installed fonts, and register outline sources\n * for the faces it returns. **Must be called from a user gesture** — the\n * permission prompt requires one, and without it the call rejects.\n *\n * Rejects when the API is missing or the user denies; neither leaves the\n * registry in a partial state.\n */\nexport async function enableLocalFontOutlines(\n opts: LocalFontOutlinesOptions = {},\n): Promise<LocalFontOutlinesResult> {\n const query = (globalThis as LocalFontWindow).queryLocalFonts;\n if (typeof query !== 'function') {\n throw new Error(\n 'weasel enableLocalFontOutlines: this browser has no Local Font Access API ' +\n '(queryLocalFonts). Large text keeps rendering from the SDF tier.',\n );\n }\n\n const wanted = opts.families ? new Set(opts.families) : null;\n const data = await query.call(globalThis);\n\n // Several installed faces can land on one (family, weight, style) slot —\n // \"Helvetica Neue Condensed Bold\" reduces to 700/normal exactly like\n // \"Helvetica Neue Bold\". Pick the least-qualified name for each slot.\n const chosen = new Map<string, { font: LocalFontData; qualifiers: number }>();\n for (const font of data) {\n if (wanted && !wanted.has(font.family)) continue;\n const { weight, style } = parseFontStyle(font.style);\n const key = `${font.family}|${weight}|${style}`;\n const qualifiers = styleQualifierCount(font.style);\n const prev = chosen.get(key);\n if (prev && prev.qualifiers <= qualifiers) continue;\n chosen.set(key, { font, qualifiers });\n }\n\n const families = new Set<string>();\n for (const { font } of chosen.values()) {\n registerFontOutlines(font.family, parseFontStyle(font.style), () => font.blob(), {\n // The PostScript name is how a member is picked out of a `.ttc`\n // collection, which is what most macOS system families ship as.\n parser: createOpenTypeParser(font.postscriptName),\n // The DOM already has every installed face; naming it avoids reading\n // the collection file a second time.\n cssSrc: `local(${JSON.stringify(font.postscriptName)})`,\n });\n families.add(font.family);\n }\n return {\n families: [...families].sort((a, b) => a.localeCompare(b)),\n faces: chosen.size,\n };\n}\n","/**\n * The GLSL that turns an atlas sample into glyph coverage, and the constants\n * naming which kind of atlas a sample came from.\n *\n * Not a program: glyphs stage into the renderer's batch alongside solid\n * geometry and image quads, and the batch shader pastes this in.\n *\n * MSDF channel layout: msdf-bmfont-xml writes R, G, and B as independent\n * signed-distance fields covering different edge directions, and the true\n * field is their median. The runtime canvas bake writes one channel that *is*\n * the field. Both encode the edge at 0.5, which is what lets one threshold\n * serve them.\n */\n\nimport type { FontStyle } from './fontStyle';\nimport { getFont } from './registerFont';\nimport { PAGE_SIZE, SDF_RADIUS } from './dynamic/dynamicAtlas';\n\n/** `a_paintMode` value for glyphs off an MSDF atlas — the median of R,G,B. */\nexport const GLYPH_MODE_MSDF = 1;\n/** `a_paintMode` value for glyphs off a runtime canvas bake — `.r` alone. */\nexport const GLYPH_MODE_R8 = 2;\n\n/**\n * Glyph coverage from one atlas sample, as GLSL for a program to paste in.\n *\n * `mode` says which field the sample carries: `GLYPH_MODE_MSDF` takes the\n * median of R,G,B, `GLYPH_MODE_R8` reads `.r` alone.\n *\n * The antialiasing band has to be one *screen* pixel wide: `fieldPerUv` is how\n * far the field runs per unit of texture coordinate on each axis (see\n * `glyphFieldScale`), and the screen derivatives of `uv` turn that into field\n * units per screen pixel.\n *\n * Not `fwidth(field)`: that is the field's slope only where the field is a\n * straight ramp. Across a stem narrower than a pixel the two columns of a 2x2\n * quad sample either side of the ridge at equal values, the derivative reads\n * flat, and the stem is thresholded away. The derivative of `uv` is constant\n * across a glyph quad, so it cannot do that. Nor a constant band, which is\n * right at one scale only.\n *\n * **Nothing here branches, and the caller must not branch around it.**\n * A derivative in non-uniform control flow is undefined, so it has to\n * be taken before anything selects on paint mode. A merged program therefore\n * runs the glyph math on fragments that are not glyphs and discards the result.\n *\n * `synthBold` shifts the threshold to thicken strokes where the resolver fell\n * back from a missing bold variant to the regular atlas.\n */\nexport const GLYPH_COVERAGE_GLSL = /* glsl */ `\nfloat median(float r, float g, float b) {\n return max(min(r, g), min(max(r, g), b));\n}\n\nfloat glyphCoverage(vec4 texel, float mode, float synthBold, vec2 uv, vec2 fieldPerUv) {\n float field = mix(median(texel.r, texel.g, texel.b), texel.r, step(1.5, mode));\n float fieldPerPx = 0.5 * (length(dFdx(uv) * fieldPerUv) + length(dFdy(uv) * fieldPerUv));\n float aaW = max(0.5 * fieldPerPx, 0.0005);\n float threshold = 0.5 - synthBold;\n return smoothstep(threshold - aaW, threshold + aaW, field);\n}\n`;\n\n/** msdf-bmfont-xml's default `distanceRange`, for an atlas that records none. */\nconst DEFAULT_MSDF_RANGE = 4;\n\n/** A runtime canvas page: `PAGE_SIZE` texels, field 0 to 1 over `SDF_RADIUS` of them. */\nconst CANVAS_FIELD_SCALE = [PAGE_SIZE / SDF_RADIUS, PAGE_SIZE / SDF_RADIUS] as const;\n\n/**\n * Field units per unit of texture coordinate along u and v for the atlas a\n * glyph group samples — the `fieldPerUv` that `glyphCoverage` takes. A baked\n * MSDF atlas answers from its page size and `distanceRange`, a runtime canvas\n * page from the dynamic tier's page size and bake radius. `undefined` for an\n * atlas that is not registered.\n */\nexport function glyphFieldScale(\n source: 'atlas' | 'canvas', family: string, weight: number, style: FontStyle,\n): readonly [number, number] | undefined {\n if (source === 'canvas') return CANVAS_FIELD_SCALE;\n const font = getFont(family, weight, style)?.font;\n if (!font) return undefined;\n const range = font.distanceRange ?? DEFAULT_MSDF_RANGE;\n return [font.common.scaleW / range, font.common.scaleH / range];\n}\n"]}