@weasel-js/font 0.7.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 orochi235
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # @weasel-js/font
2
+
3
+ MSDF font atlases, glyph metrics, and runtime glyph rasterization for
4
+ `@weasel-js/core`.
5
+
6
+ A Tier A leaf: core depends on this package, never the reverse. It owns the
7
+ font registry — the one piece of module-level state whose duplication renders
8
+ no glyphs at all.
9
+
10
+ ## What's here
11
+
12
+ | Module | Role |
13
+ | --- | --- |
14
+ | `FontAtlas` | BMFont metrics parsing |
15
+ | `registerFont` | The registry, variant resolution, texture upload |
16
+ | `dynamic/` | Runtime canvas-SDF rasterization for glyphs with no baked atlas |
17
+ | `textureSink` | The `GlyphTextureSink` seam — the renderer injects GL texture upload, so this package never imports one |
18
+ | `textSdf` | Shader source for the SDF text program |
19
+
20
+ ## Fallback
21
+
22
+ An unregistered family renders in the default family with a one-time warning:
23
+
24
+ ```ts
25
+ setFontFallbackPolicy('substitute'); // default — render in the default family
26
+ setFontFallbackPolicy('canvas'); // rasterize the real typeface at runtime
27
+ setFontFallbackPolicy('none'); // hard miss: render nothing (pre-0.7 behavior)
28
+
29
+ setDefaultFontFamily('Inter'); // defaults to the first registered family
30
+ ```
31
+
32
+ Substitution changes advance widths, so measurement and wrap differ from the
33
+ requested font. `ResolveResult.substituted` reports it structurally so a UI can
34
+ surface the swap rather than leaving it to the console.
35
+
36
+ ## Generating an atlas
37
+
38
+ `npm run gen:font` (source in `scripts/gen-font.ts`).
@@ -0,0 +1,395 @@
1
+ /**
2
+ * BmFont JSON types and parser for the MSDF atlas format produced by
3
+ * msdf-bmfont-xml. The parser builds accelerator maps (charMap, kerningMap)
4
+ * for O(1) glyph and kerning lookup during layout.
5
+ */
6
+ interface BmFontInfo {
7
+ face: string;
8
+ size: number;
9
+ }
10
+ interface BmFontCommon {
11
+ lineHeight: number;
12
+ base: number;
13
+ scaleW: number;
14
+ scaleH: number;
15
+ }
16
+ interface BmFontChar {
17
+ id: number;
18
+ x: number;
19
+ y: number;
20
+ width: number;
21
+ height: number;
22
+ xoffset: number;
23
+ yoffset: number;
24
+ xadvance: number;
25
+ page: number;
26
+ }
27
+ interface BmFontKerning {
28
+ first: number;
29
+ second: number;
30
+ amount: number;
31
+ }
32
+ interface BmFont {
33
+ info: BmFontInfo;
34
+ common: BmFontCommon;
35
+ chars: BmFontChar[];
36
+ kernings: BmFontKerning[];
37
+ charMap: Map<number, BmFontChar>;
38
+ kerningMap: Map<number, Map<number, number>>;
39
+ }
40
+ /** Two-glyph fixture for unit tests. */
41
+ declare const FIXTURE_FONT: {
42
+ info: {
43
+ face: string;
44
+ size: number;
45
+ };
46
+ common: {
47
+ lineHeight: number;
48
+ base: number;
49
+ scaleW: number;
50
+ scaleH: number;
51
+ };
52
+ chars: {
53
+ id: number;
54
+ x: number;
55
+ y: number;
56
+ width: number;
57
+ height: number;
58
+ xoffset: number;
59
+ yoffset: number;
60
+ xadvance: number;
61
+ page: number;
62
+ }[];
63
+ kernings: {
64
+ first: number;
65
+ second: number;
66
+ amount: number;
67
+ }[];
68
+ };
69
+ declare function parseBmFont(raw: unknown): BmFont;
70
+
71
+ /**
72
+ * The only thing the glyph tier needs from a GL texture cache. Core's
73
+ * `GLTextureCache` satisfies this structurally — there is no adapter and no
74
+ * registration step, because the type *is* the seam.
75
+ *
76
+ * Declared here rather than imported so this package has zero reach-back into
77
+ * core. If core's cache ever drops one of these methods, the failure surfaces
78
+ * as a type error at the call site in `renderer/draw.ts`, which is where it
79
+ * belongs.
80
+ */
81
+ /** Anything WebGL can upload as a texture. */
82
+ type TexSource = HTMLImageElement | ImageBitmap | ImageData | HTMLCanvasElement;
83
+ interface GlyphTextureSink {
84
+ has(id: string): boolean;
85
+ upload(id: string, source: TexSource): string;
86
+ uploadR8(id: string, width: number, height: number, data: Uint8Array): void;
87
+ subImageR8(id: string, x: number, y: number, w: number, h: number, data: Uint8Array): void;
88
+ }
89
+
90
+ interface FaceMetrics {
91
+ ascent: number;
92
+ descent: number;
93
+ }
94
+ interface RasterizedGlyph {
95
+ width: number;
96
+ height: number;
97
+ alpha: Uint8ClampedArray;
98
+ left: number;
99
+ top: number;
100
+ advance: number;
101
+ }
102
+ interface GlyphRasterizer {
103
+ faceMetrics(family: string, weight: number, style: 'normal' | 'italic'): FaceMetrics;
104
+ rasterize(family: string, weight: number, style: 'normal' | 'italic', codepoint: number): RasterizedGlyph;
105
+ }
106
+
107
+ /**
108
+ * DynamicGlyphAtlas — runtime single-channel SDF glyphs for canvas-sourced
109
+ * (installed machine) fonts. TinySDF technique: canvas fillText at 48 px →
110
+ * Euclidean distance transform → shelf-packed R8 pages (1024², max 4).
111
+ *
112
+ * Baked MSDF atlases always win: this tier only serves families registered
113
+ * via `registerCanvasFont` that have no baked entry (see resolveFontVariant).
114
+ *
115
+ * Faces expose a BmFont-shaped `font` whose charMap grows as glyphs are
116
+ * requested, so `layoutRuns` consumes them through the same code path as
117
+ * baked atlases. A char's advance is valid immediately (measureText); its
118
+ * atlas rect fills in when the bake lands (width 0 / page -1 until then, so
119
+ * layout advances the pen but emits no quad).
120
+ */
121
+
122
+ declare const DEFAULT_BAKE_BUDGET = 16;
123
+ interface DynamicFace {
124
+ family: string;
125
+ weight: number;
126
+ style: 'normal' | 'italic';
127
+ /** BmFont-shaped view consumed by layoutRuns; charMap grows lazily. */
128
+ font: BmFont;
129
+ /** Char record for `cp`, measured on first request (advance always valid
130
+ * immediately; atlas rect fills in when the bake lands). */
131
+ requestGlyph(cp: number): BmFontChar;
132
+ }
133
+ interface DynamicPage {
134
+ data: Uint8Array;
135
+ /** Bumps on every glyph blit; each blit appends a patch with seq = version. */
136
+ version: number;
137
+ patches: {
138
+ seq: number;
139
+ x: number;
140
+ y: number;
141
+ w: number;
142
+ h: number;
143
+ }[];
144
+ }
145
+ /** Mark `family` as canvas-sourced: when no baked atlas covers it,
146
+ * resolveFontVariant serves it from this dynamic atlas. */
147
+ declare function registerCanvasFont(family: string): void;
148
+ /**
149
+ * Will `family` be served by the dynamic canvas-SDF tier *right now*?
150
+ *
151
+ * Service, not membership — the answer depends on the fallback policy in
152
+ * force and can change without any enrollment call:
153
+ * - explicitly enrolled via `registerCanvasFont` → `true` under every
154
+ * policy; a consumer naming a family outranks the policy.
155
+ * - auto-enrolled by the `'canvas'` policy → `true` only while that policy
156
+ * is still in force. The enrollment lapses rather than being discarded,
157
+ * so returning to `'canvas'` makes it `true` again.
158
+ * - never enrolled → `false`, including under `'canvas'` (that policy
159
+ * enrolls lazily, on the first miss).
160
+ *
161
+ * The membership reading would answer `true` for an auto-enrolled family
162
+ * under `'substitute'` / `'none'`, where nothing routes to this tier — a
163
+ * caller predicting what renders would be told the opposite of the truth.
164
+ * Mirrors `listFonts`, which reports the baked registry alone for the same
165
+ * reason: "enrolled" is not "will render".
166
+ */
167
+ declare function isCanvasFont(family: string): boolean;
168
+ /** Remove a canvas family. Its faces are dropped; already-baked glyph
169
+ * pixels stay in their pages (no eviction in v1). */
170
+ declare function unregisterCanvasFont(family: string): void;
171
+ /** Subscribe to deferred-bake completion (fires after each flush batch).
172
+ * `<SceneCanvas>` subscribes and requests a redraw, mirroring
173
+ * `subscribeImageReady`. Returns an unsubscribe. */
174
+ declare function subscribeGlyphReady(cb: () => void): () => void;
175
+ /** Reset the per-frame synchronous bake budget. Called by
176
+ * `WeaselRenderer.render()` at frame start; the headless
177
+ * `renderSceneToPixels` path passes Infinity so print never defers. */
178
+ declare function resetBakeBudget(n?: number): void;
179
+ /** Texture-cache key for a dynamic page (parallel to `textureCacheKey`). */
180
+ declare function dynamicPageTextureId(page: number): string;
181
+ /** Bring `cache`'s copy of page `pageIndex` up to date: full R8 upload the
182
+ * first time, `texSubImage2D` patches after. Returns false if the page
183
+ * doesn't exist yet. */
184
+ declare function syncDynamicPageTexture(cache: GlyphTextureSink, pageIndex: number): boolean;
185
+ /** @internal test seam — inject a fake rasterizer (jsdom has no canvas
186
+ * metrics). Pass null to restore the lazy default. */
187
+ declare function __setGlyphRasterizerForTests(r: GlyphRasterizer | null): void;
188
+ /** @internal test seam — inspect CPU-side pages. */
189
+ declare function _getPagesForTests(): readonly DynamicPage[];
190
+ /** @internal test seam — clear all dynamic-font state. */
191
+ declare function _resetDynamicFontsForTests(): void;
192
+
193
+ /**
194
+ * FontRegistry and registerFont() public API.
195
+ *
196
+ * Variants are keyed by (family, weight, style). registerFont() takes a
197
+ * FontVariant alongside the family and the two URLs; the registry stores
198
+ * entries in a two-level Map so resolveFontVariant() can iterate a family's
199
+ * variants for the fallback chain.
200
+ */
201
+
202
+ interface FontEntry {
203
+ font: BmFont;
204
+ bitmap: ImageBitmap;
205
+ }
206
+ interface FontVariant {
207
+ weight?: number;
208
+ style?: 'normal' | 'italic';
209
+ }
210
+ type FontStyle = 'normal' | 'italic';
211
+ /** Test helper. Do not call from product code. */
212
+ declare function _resetFontRegistryForTests(): void;
213
+ /** Exact lookup — does NOT walk the fallback chain. Use `resolveFontVariant` for that. */
214
+ declare function getFont(family: string, weight?: number, style?: FontStyle): FontEntry | null;
215
+ interface RegisteredFont {
216
+ family: string;
217
+ variants: readonly {
218
+ weight: number;
219
+ style: FontStyle;
220
+ }[];
221
+ }
222
+ /**
223
+ * Enumerate the registry — what a font picker can honestly offer. Families
224
+ * come back in registration order; variants sorted by weight, then style, so
225
+ * the output is stable enough to assert against.
226
+ */
227
+ declare function listFonts(): readonly RegisteredFont[];
228
+ declare function registerFont(family: string, variant: FontVariant, metricsUrl: string, atlasUrl: string): Promise<void>;
229
+ /**
230
+ * Ensure the atlas for `(family, weight, style)` is uploaded to
231
+ * `textureCache`. Cache key is `${family}|${weight}|${style}` so each
232
+ * variant occupies its own texture slot.
233
+ */
234
+ declare function ensureFontTexture(family: string, weight: number, style: FontStyle, textureCache: GlyphTextureSink): boolean;
235
+ /** The texture cache key used by `ensureFontTexture` for a given variant. */
236
+ declare function textureCacheKey(family: string, weight: number, style: FontStyle): string;
237
+ /** Kept as a no-op for context-restore call sites; per-cache dedup handles it now. */
238
+ declare function markAllFontsNotUploaded(): void;
239
+ interface ResolveResult {
240
+ entry: FontEntry | null;
241
+ /**
242
+ * The (family, weight, style) triple that was actually matched. May differ
243
+ * from the requested values when the resolver walked the fallback chain —
244
+ * including `family`, when the cross-family policy substituted a default.
245
+ *
246
+ * This is the atlas identity: pass all three to `getFont` /
247
+ * `textureCacheKey` and the lookup hits. Describing only weight and style
248
+ * here once let a caller key its draw on the *requested* family, which
249
+ * resolves to no atlas at all and paints nothing.
250
+ *
251
+ * The synthetic flags describe the gap between requested and resolved for
252
+ * shader-side compensation. When `entry` is null, these mirror the
253
+ * requested values.
254
+ */
255
+ resolved: {
256
+ family: string;
257
+ weight: number;
258
+ style: FontStyle;
259
+ };
260
+ synthetic: {
261
+ bold: boolean;
262
+ italic: boolean;
263
+ };
264
+ /** Which tier resolved: a baked MSDF atlas, or the runtime canvas-SDF
265
+ * dynamic atlas. Misses report 'atlas' (the default tier). */
266
+ source: 'atlas' | 'canvas';
267
+ /** Set only when source === 'canvas': the dynamic face whose BmFont-shaped
268
+ * `font` layoutRuns consumes in place of `entry.font`. */
269
+ dynamicFace?: DynamicFace;
270
+ /**
271
+ * Set when the requested family was not registered and the fallback policy
272
+ * substituted a different one. Reported structurally so a UI can say
273
+ * "Inter — not loaded, showing Roboto" instead of leaving the user to
274
+ * wonder why the family control did nothing.
275
+ */
276
+ substituted?: {
277
+ requested: string;
278
+ resolved: string;
279
+ };
280
+ }
281
+ /**
282
+ * Resolve a `(family, weight, style)` request to a registered font entry,
283
+ * walking the fallback chain when an exact match isn't available. Returns
284
+ * synthetic flags describing the gap between requested and resolved so the
285
+ * renderer can apply SDF-thicken / vertex-skew fakes.
286
+ */
287
+ declare function resolveFontVariant(family: string, weight: number, style: FontStyle): ResolveResult;
288
+ /**
289
+ * Escalate a *single codepoint* to the dynamic tier when the atlas that
290
+ * resolved for the run has no glyph for it.
291
+ *
292
+ * `resolveFontVariant` answers at family granularity: it picks one tier for a
293
+ * whole run. But a baked MSDF atlas covers a fixed charset, so a run served by
294
+ * a perfectly good atlas can still contain a character that atlas never baked
295
+ * — an em dash, a curly quote, anything outside the subset. The dynamic tier
296
+ * rasterizes on demand from installed fonts and can serve exactly those.
297
+ *
298
+ * Returns a canvas-tier `ResolveResult` whose `dynamicFace` the caller drives
299
+ * with `requestGlyph(cp)`, or `null` when escalation isn't available:
300
+ *
301
+ * - Policy `'none'` documents a miss as a *hard* miss. Quietly reaching for
302
+ * another tier per codepoint would undo that, so it doesn't.
303
+ * - No canvas to rasterize into (SSR, a jsdom test without one) makes the
304
+ * dynamic tier constructible-but-broken; `getDynamicFace` throws and this
305
+ * reports the miss instead of taking the caller down with it.
306
+ *
307
+ * Cheap to call per missing codepoint: faces are cached by variant and glyphs
308
+ * by codepoint, so a repeat is two map lookups.
309
+ */
310
+ declare function resolveGlyphFallback(family: string, weight: number, style: FontStyle): ResolveResult | null;
311
+
312
+ /**
313
+ * Cross-family fallback policy. The per-family chain in `resolveFontVariant`
314
+ * (weight → style → synthetic) has always been rich; what was missing is what
315
+ * happens when the family itself was never registered. That case used to
316
+ * render nothing at all, which is the single most common cause of "my text is
317
+ * invisible".
318
+ */
319
+ /**
320
+ * What happens when a requested family has no baked atlas:
321
+ * - `'substitute'` — render with the default family (see
322
+ * `setDefaultFontFamily`), reporting the swap on `ResolveResult.substituted`.
323
+ * - `'canvas'` — auto-enroll the family with the dynamic canvas-SDF
324
+ * rasterizer. The consumer gets the *real* typeface if the browser has it,
325
+ * at canvas-SDF quality rather than baked-MSDF quality.
326
+ * - `'none'` — hard miss; the run renders nothing.
327
+ */
328
+ type FontFallbackPolicy = 'substitute' | 'canvas' | 'none';
329
+ declare function setFontFallbackPolicy(next: FontFallbackPolicy): void;
330
+ declare function getFontFallbackPolicy(): FontFallbackPolicy;
331
+ /** Explicit default family for `'substitute'`. When unset, the first
332
+ * registered family wins — the right answer for the common case of an app
333
+ * that registers exactly one. */
334
+ declare function setDefaultFontFamily(family: string): void;
335
+ declare function getDefaultFontFamily(): string | null;
336
+ /** Test helper. Do not call from product code. */
337
+ declare function _resetFallbackForTests(): void;
338
+
339
+ /**
340
+ * GLSL ES 3.0 sources for the built-in MSDF text shader.
341
+ *
342
+ * Vertex inputs (interleaved, stride 20 bytes = 5 × float):
343
+ * a_position vec2 screen-space x,y of the glyph quad vertex
344
+ * a_uv vec2 atlas UV (0..1)
345
+ * a_baselineY float line baseline Y in screen space (for synth-italic skew)
346
+ *
347
+ * Uniforms:
348
+ * u_proj mat3 screen → clip projection
349
+ * u_model mat3 cumulative group transform
350
+ * u_atlas sampler2D the MSDF atlas texture (bound to TEXTURE0)
351
+ * u_color vec4 text color (straight RGBA)
352
+ * u_alpha float group alpha multiplier
353
+ * u_colorMatrix mat4 color transform applied to u_color before alpha modulation
354
+ * u_colorBias vec4 bias added after the matrix (identity = zero bias)
355
+ *
356
+ * Output: PREMULTIPLIED alpha — `vec4(color.rgb * a, a)` per conventions §2.
357
+ * Blend func: ONE / ONE_MINUS_SRC_ALPHA.
358
+ *
359
+ * MSDF channel layout: msdf-bmfont-xml outputs R,G,B channels as independent
360
+ * signed-distance fields covering different edge directions. The true SDF
361
+ * value is the median of R,G,B; this recovers sharp outlines while averaging
362
+ * out single-channel aliasing artifacts.
363
+ *
364
+ * Antialiasing (`aaWidth`, both shaders): the smoothstep band must be one
365
+ * *screen* pixel wide, so it is derived per-fragment from `fwidth(sdfVal)` —
366
+ * the rate the field changes between adjacent fragments. That single quantity
367
+ * already folds in font size, zoom, and DPR: minify the glyph and the field
368
+ * changes faster, so the band widens in field units to stay one pixel on
369
+ * screen; magnify it and the band narrows.
370
+ *
371
+ * A *constant* band cannot be correct at more than one scale, and this shader
372
+ * used one (0.05) until 2026-07-29. At 16px text the band collapsed to well
373
+ * under a pixel and glyph edges quantized to hard stair-steps; at display
374
+ * sizes the same constant read mushy. `fwidth` is core in GLSL ES 3.00, so
375
+ * no extension guard is needed. The `max()` floor keeps a degenerate
376
+ * derivative (flat field, or a driver returning 0) from producing a
377
+ * zero-width band, which would be the aliased behavior all over again.
378
+ */
379
+ declare const TEXT_VERT_SRC = "#version 300 es\nin vec2 a_position;\nin vec2 a_uv;\nin float a_baselineY;\nuniform mat3 u_proj;\nuniform mat3 u_model;\nuniform float u_synthItalic;\nout vec2 v_uv;\nvoid main() {\n // Synthetic italic: shift x by (a_baselineY - a_position.y) * tan(angle).\n // Above-baseline vertices (lower y in screen coords) lean further right.\n vec2 skewed = vec2(\n a_position.x + (a_baselineY - a_position.y) * tan(u_synthItalic),\n a_position.y\n );\n vec3 screen = u_model * vec3(skewed, 1.0);\n vec3 clip = u_proj * vec3(screen.xy, 1.0);\n gl_Position = vec4(clip.xy, 0.0, 1.0);\n v_uv = a_uv;\n}\n";
380
+ declare const TEXT_FRAG_SRC = "#version 300 es\nprecision highp float;\nin vec2 v_uv;\nuniform sampler2D u_atlas;\nuniform vec4 u_color;\nuniform float u_alpha;\nuniform float u_synthBold;\nuniform mat4 u_colorMatrix;\nuniform vec4 u_colorBias;\nout vec4 outColor;\n\nfloat median(float r, float g, float b) {\n return max(min(r, g), min(max(r, g), b));\n}\n\nvoid main() {\n vec3 sdf = texture(u_atlas, v_uv).rgb;\n float sdfVal = median(sdf.r, sdf.g, sdf.b);\n // Screen-space AA band \u2014 see the file header. Half of fwidth spans ~1px.\n float aaW = max(0.5 * fwidth(sdfVal), 0.0005);\n // u_synthBold shifts the SDF threshold to thicken strokes when the\n // resolver fell back from a missing bold variant to the regular atlas.\n float threshold = 0.5 - u_synthBold;\n float msdfAlpha = smoothstep(threshold - aaW, threshold + aaW, sdfVal);\n vec4 src = vec4(u_color.rgb, u_color.a);\n vec4 mapped = clamp(u_colorMatrix * src + u_colorBias, 0.0, 1.0);\n float a = mapped.a * msdfAlpha * u_alpha;\n outColor = vec4(mapped.rgb * a, a);\n}\n";
381
+ /**
382
+ * Single-channel sibling of TEXT_FRAG_SRC for runtime canvas-SDF glyphs
383
+ * (DynamicGlyphAtlas R8 pages): the R channel IS the distance field, so no
384
+ * median. Threshold semantics (0.5 edge, u_synthBold shift) match the MSDF
385
+ * shader because the bake encodes the edge at ~128.
386
+ *
387
+ * Accepted trade: corner rounding away from the bake size, mildest near it.
388
+ * `glyphRasterizer.ts` carries the measurements and the reason neither a
389
+ * larger bake nor extra taps would improve the small-text end.
390
+ */
391
+ declare const TEXT_FRAG_R8_SRC = "#version 300 es\nprecision highp float;\nin vec2 v_uv;\nuniform sampler2D u_atlas;\nuniform vec4 u_color;\nuniform float u_alpha;\nuniform float u_synthBold;\nuniform mat4 u_colorMatrix;\nuniform vec4 u_colorBias;\nout vec4 outColor;\n\nvoid main() {\n float sdfVal = texture(u_atlas, v_uv).r;\n // Screen-space AA band \u2014 see the file header. Half of fwidth spans ~1px.\n float aaW = max(0.5 * fwidth(sdfVal), 0.0005);\n float threshold = 0.5 - u_synthBold;\n float sdfAlpha = smoothstep(threshold - aaW, threshold + aaW, sdfVal);\n vec4 src = vec4(u_color.rgb, u_color.a);\n vec4 mapped = clamp(u_colorMatrix * src + u_colorBias, 0.0, 1.0);\n float a = mapped.a * sdfAlpha * u_alpha;\n outColor = vec4(mapped.rgb * a, a);\n}\n";
392
+ declare const TEXT_SDF_UNIFORMS: readonly ["u_proj", "u_model", "u_atlas", "u_color", "u_alpha", "u_synthBold", "u_synthItalic", "u_colorMatrix", "u_colorBias"];
393
+ declare const TEXT_SDF_ATTRIBUTES: readonly ["a_position", "a_uv", "a_baselineY"];
394
+
395
+ export { type BmFont, type BmFontChar, type BmFontCommon, type BmFontInfo, type BmFontKerning, DEFAULT_BAKE_BUDGET, FIXTURE_FONT, type FontEntry, type FontFallbackPolicy, type FontVariant, type GlyphTextureSink, type RegisteredFont, type ResolveResult, TEXT_FRAG_R8_SRC, TEXT_FRAG_SRC, TEXT_SDF_ATTRIBUTES, TEXT_SDF_UNIFORMS, TEXT_VERT_SRC, type TexSource, __setGlyphRasterizerForTests, _getPagesForTests, _resetDynamicFontsForTests, _resetFallbackForTests, _resetFontRegistryForTests, dynamicPageTextureId, ensureFontTexture, getDefaultFontFamily, getFont, getFontFallbackPolicy, isCanvasFont, listFonts, markAllFontsNotUploaded, parseBmFont, registerCanvasFont, registerFont, resetBakeBudget, resolveFontVariant, resolveGlyphFallback, setDefaultFontFamily, setFontFallbackPolicy, subscribeGlyphReady, syncDynamicPageTexture, textureCacheKey, unregisterCanvasFont };