@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 +21 -0
- package/README.md +38 -0
- package/dist/index.d.ts +395 -0
- package/dist/index.js +825 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
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`).
|
package/dist/index.d.ts
ADDED
|
@@ -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 };
|