@weasel-js/font 1.0.2 → 1.0.4
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/chunk-3RSTZDXH.js +1008 -0
- package/dist/chunk-3RSTZDXH.js.map +1 -0
- package/dist/index.d.ts +11 -514
- package/dist/index.js +22 -1011
- package/dist/index.js.map +1 -1
- package/dist/test-seams-DLNsXb8P.d.ts +562 -0
- package/dist/test-seams.d.ts +1 -0
- package/dist/test-seams.js +3 -0
- package/dist/test-seams.js.map +1 -0
- package/package.json +5 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,361 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
/** One family served by the dynamic canvas-SDF tier. See `listCanvasFonts`. */
|
|
146
|
-
interface CanvasFontEntry {
|
|
147
|
-
family: string;
|
|
148
|
-
/** `'explicit'` — a `registerCanvasFont` call; `'auto'` — the `'canvas'`
|
|
149
|
-
* fallback policy enrolled it on a miss, and the enrollment lapses if
|
|
150
|
-
* that policy changes. */
|
|
151
|
-
enrollment: 'explicit' | 'auto';
|
|
152
|
-
}
|
|
153
|
-
/** Mark `family` as canvas-sourced: when no baked atlas covers it,
|
|
154
|
-
* resolveFontVariant serves it from this dynamic atlas. */
|
|
155
|
-
declare function registerCanvasFont(family: string): void;
|
|
156
|
-
/**
|
|
157
|
-
* Will `family` be served by the dynamic canvas-SDF tier *right now*?
|
|
158
|
-
*
|
|
159
|
-
* Service, not membership — the answer depends on the fallback policy in
|
|
160
|
-
* force and can change without any enrollment call:
|
|
161
|
-
* - explicitly enrolled via `registerCanvasFont` → `true` under every
|
|
162
|
-
* policy; a consumer naming a family outranks the policy.
|
|
163
|
-
* - auto-enrolled by the `'canvas'` policy → `true` only while that policy
|
|
164
|
-
* is still in force. The enrollment lapses rather than being discarded,
|
|
165
|
-
* so returning to `'canvas'` makes it `true` again.
|
|
166
|
-
* - never enrolled → `false`, including under `'canvas'` (that policy
|
|
167
|
-
* enrolls lazily, on the first miss).
|
|
168
|
-
*
|
|
169
|
-
* The membership reading would answer `true` for an auto-enrolled family
|
|
170
|
-
* under `'substitute'` / `'none'`, where nothing routes to this tier — a
|
|
171
|
-
* caller predicting what renders would be told the opposite of the truth.
|
|
172
|
-
* Mirrors `listFonts`, which reports the baked registry alone for the same
|
|
173
|
-
* reason: "enrolled" is not "will render".
|
|
174
|
-
*/
|
|
175
|
-
declare function isCanvasFont(family: string): boolean;
|
|
176
|
-
/**
|
|
177
|
-
* Every family the dynamic canvas-SDF tier will serve *right now* — the
|
|
178
|
-
* enumeration companion to `isCanvasFont`, which answers one family at a
|
|
179
|
-
* time. Same "service, not membership" rule: an auto-enrolled family appears
|
|
180
|
-
* only while the `'canvas'` policy is in force.
|
|
181
|
-
*
|
|
182
|
-
* Exists so a consumer can offer these families in a font picker.
|
|
183
|
-
* `listFonts` reports the baked registry alone, so without this the only
|
|
184
|
-
* ways to present canvas-served families were to hard-code a list beside the
|
|
185
|
-
* `registerCanvasFont` calls or to omit them — a UI inventing its own answer
|
|
186
|
-
* about what will render.
|
|
187
|
-
*
|
|
188
|
-
* `enrollment` distinguishes a consumer's explicit `registerCanvasFont` from
|
|
189
|
-
* a lazy enrollment the policy made on a miss; both render identically, but
|
|
190
|
-
* only the explicit one survives a policy change.
|
|
191
|
-
*/
|
|
192
|
-
declare function listCanvasFonts(): readonly CanvasFontEntry[];
|
|
193
|
-
/** Remove a canvas family. Its faces are dropped; already-baked glyph
|
|
194
|
-
* pixels stay in their pages (no eviction in v1). */
|
|
195
|
-
declare function unregisterCanvasFont(family: string): void;
|
|
196
|
-
/** Reset the per-frame synchronous bake budget. Called by
|
|
197
|
-
* `WeaselRenderer.render()` at frame start; the headless
|
|
198
|
-
* `renderSceneToPixels` path passes Infinity so print never defers. */
|
|
199
|
-
declare function resetBakeBudget(n?: number): void;
|
|
200
|
-
/** Texture-cache key for a dynamic page (parallel to `textureCacheKey`). */
|
|
201
|
-
declare function dynamicPageTextureId(page: number): string;
|
|
202
|
-
/** Bring `cache`'s copy of page `pageIndex` up to date: full R8 upload the
|
|
203
|
-
* first time, `texSubImage2D` patches after. Returns false if the page
|
|
204
|
-
* doesn't exist yet. */
|
|
205
|
-
declare function syncDynamicPageTexture(cache: GlyphTextureSink, pageIndex: number): boolean;
|
|
206
|
-
/** @internal test seam — inject a fake rasterizer (jsdom has no canvas
|
|
207
|
-
* metrics). Pass null to restore the lazy default. */
|
|
208
|
-
declare function __setGlyphRasterizerForTests(r: GlyphRasterizer | null): void;
|
|
209
|
-
/** @internal test seam — inspect CPU-side pages. */
|
|
210
|
-
declare function _getPagesForTests(): readonly DynamicPage[];
|
|
211
|
-
/** @internal test seam — clear all dynamic-font state. */
|
|
212
|
-
declare function _resetDynamicFontsForTests(): void;
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* FontRegistry and registerFont() public API.
|
|
216
|
-
*
|
|
217
|
-
* Variants are keyed by (family, weight, style). registerFont() takes a
|
|
218
|
-
* FontVariant alongside the family and the two URLs; the registry stores
|
|
219
|
-
* entries in a two-level Map so resolveFontVariant() can iterate a family's
|
|
220
|
-
* variants for the fallback chain.
|
|
221
|
-
*/
|
|
222
|
-
|
|
223
|
-
interface FontEntry {
|
|
224
|
-
font: BmFont;
|
|
225
|
-
bitmap: ImageBitmap;
|
|
226
|
-
}
|
|
227
|
-
interface FontVariant {
|
|
228
|
-
weight?: number;
|
|
229
|
-
style?: 'normal' | 'italic';
|
|
230
|
-
}
|
|
231
|
-
type FontStyle = 'normal' | 'italic';
|
|
232
|
-
/** Test helper. Do not call from product code. */
|
|
233
|
-
declare function _resetFontRegistryForTests(): void;
|
|
234
|
-
/** Exact lookup — does NOT walk the fallback chain. Use `resolveFontVariant` for that. */
|
|
235
|
-
declare function getFont(family: string, weight?: number, style?: FontStyle): FontEntry | null;
|
|
236
|
-
interface RegisteredFont {
|
|
237
|
-
family: string;
|
|
238
|
-
variants: readonly {
|
|
239
|
-
weight: number;
|
|
240
|
-
style: FontStyle;
|
|
241
|
-
}[];
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* Enumerate the registry — what a font picker can honestly offer. Families
|
|
245
|
-
* come back in registration order; variants sorted by weight, then style, so
|
|
246
|
-
* the output is stable enough to assert against.
|
|
247
|
-
*/
|
|
248
|
-
declare function listFonts(): readonly RegisteredFont[];
|
|
249
|
-
declare function registerFont(family: string, variant: FontVariant, metricsUrl: string, atlasUrl: string): Promise<void>;
|
|
250
|
-
/**
|
|
251
|
-
* Ensure the atlas for `(family, weight, style)` is uploaded to
|
|
252
|
-
* `textureCache`. Cache key is `${family}|${weight}|${style}` so each
|
|
253
|
-
* variant occupies its own texture slot.
|
|
254
|
-
*/
|
|
255
|
-
declare function ensureFontTexture(family: string, weight: number, style: FontStyle, textureCache: GlyphTextureSink): boolean;
|
|
256
|
-
/** The texture cache key used by `ensureFontTexture` for a given variant. */
|
|
257
|
-
declare function textureCacheKey(family: string, weight: number, style: FontStyle): string;
|
|
258
|
-
/** Kept as a no-op for context-restore call sites; per-cache dedup handles it now. */
|
|
259
|
-
declare function markAllFontsNotUploaded(): void;
|
|
260
|
-
interface ResolveResult {
|
|
261
|
-
entry: FontEntry | null;
|
|
262
|
-
/**
|
|
263
|
-
* The (family, weight, style) triple that was actually matched. May differ
|
|
264
|
-
* from the requested values when the resolver walked the fallback chain —
|
|
265
|
-
* including `family`, when the cross-family policy substituted a default.
|
|
266
|
-
*
|
|
267
|
-
* This is the atlas identity: pass all three to `getFont` /
|
|
268
|
-
* `textureCacheKey` and the lookup hits. Describing only weight and style
|
|
269
|
-
* here once let a caller key its draw on the *requested* family, which
|
|
270
|
-
* resolves to no atlas at all and paints nothing.
|
|
271
|
-
*
|
|
272
|
-
* The synthetic flags describe the gap between requested and resolved for
|
|
273
|
-
* shader-side compensation. When `entry` is null, these mirror the
|
|
274
|
-
* requested values.
|
|
275
|
-
*/
|
|
276
|
-
resolved: {
|
|
277
|
-
family: string;
|
|
278
|
-
weight: number;
|
|
279
|
-
style: FontStyle;
|
|
280
|
-
};
|
|
281
|
-
synthetic: {
|
|
282
|
-
bold: boolean;
|
|
283
|
-
italic: boolean;
|
|
284
|
-
};
|
|
285
|
-
/** Which tier resolved: a baked MSDF atlas, or the runtime canvas-SDF
|
|
286
|
-
* dynamic atlas. Misses report 'atlas' (the default tier). */
|
|
287
|
-
source: 'atlas' | 'canvas';
|
|
288
|
-
/** Set only when source === 'canvas': the dynamic face whose BmFont-shaped
|
|
289
|
-
* `font` layoutRuns consumes in place of `entry.font`. */
|
|
290
|
-
dynamicFace?: DynamicFace;
|
|
291
|
-
/**
|
|
292
|
-
* Set when the requested family was not registered and the fallback policy
|
|
293
|
-
* substituted a different one. Reported structurally so a UI can say
|
|
294
|
-
* "Inter — not loaded, showing Roboto" instead of leaving the user to
|
|
295
|
-
* wonder why the family control did nothing.
|
|
296
|
-
*/
|
|
297
|
-
substituted?: {
|
|
298
|
-
requested: string;
|
|
299
|
-
resolved: string;
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
/**
|
|
303
|
-
* Resolve a `(family, weight, style)` request to a registered font entry,
|
|
304
|
-
* walking the fallback chain when an exact match isn't available. Returns
|
|
305
|
-
* synthetic flags describing the gap between requested and resolved so the
|
|
306
|
-
* renderer can apply SDF-thicken / vertex-skew fakes.
|
|
307
|
-
*/
|
|
308
|
-
declare function resolveFontVariant(family: string, weight: number, style: FontStyle): ResolveResult;
|
|
309
|
-
/**
|
|
310
|
-
* Escalate a *single codepoint* to the dynamic tier when the atlas that
|
|
311
|
-
* resolved for the run has no glyph for it.
|
|
312
|
-
*
|
|
313
|
-
* `resolveFontVariant` answers at family granularity: it picks one tier for a
|
|
314
|
-
* whole run. But a baked MSDF atlas covers a fixed charset, so a run served by
|
|
315
|
-
* a perfectly good atlas can still contain a character that atlas never baked
|
|
316
|
-
* — an em dash, a curly quote, anything outside the subset. The dynamic tier
|
|
317
|
-
* rasterizes on demand from installed fonts and can serve exactly those.
|
|
318
|
-
*
|
|
319
|
-
* Returns a canvas-tier `ResolveResult` whose `dynamicFace` the caller drives
|
|
320
|
-
* with `requestGlyph(cp)`, or `null` when escalation isn't available:
|
|
321
|
-
*
|
|
322
|
-
* - Policy `'none'` documents a miss as a *hard* miss. Quietly reaching for
|
|
323
|
-
* another tier per codepoint would undo that, so it doesn't.
|
|
324
|
-
* - No canvas to rasterize into (SSR, a jsdom test without one) makes the
|
|
325
|
-
* dynamic tier constructible-but-broken; `getDynamicFace` throws and this
|
|
326
|
-
* reports the miss instead of taking the caller down with it.
|
|
327
|
-
*
|
|
328
|
-
* Cheap to call per missing codepoint: faces are cached by variant and glyphs
|
|
329
|
-
* by codepoint, so a repeat is two map lookups.
|
|
330
|
-
*/
|
|
331
|
-
declare function resolveGlyphFallback(family: string, weight: number, style: FontStyle): ResolveResult | null;
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Cross-family fallback policy. The per-family chain in `resolveFontVariant`
|
|
335
|
-
* (weight → style → synthetic) has always been rich; what was missing is what
|
|
336
|
-
* happens when the family itself was never registered. That case used to
|
|
337
|
-
* render nothing at all, which is the single most common cause of "my text is
|
|
338
|
-
* invisible".
|
|
339
|
-
*/
|
|
340
|
-
/**
|
|
341
|
-
* What happens when a requested family has no baked atlas:
|
|
342
|
-
* - `'substitute'` — render with the default family (see
|
|
343
|
-
* `setDefaultFontFamily`), reporting the swap on `ResolveResult.substituted`.
|
|
344
|
-
* - `'canvas'` — auto-enroll the family with the dynamic canvas-SDF
|
|
345
|
-
* rasterizer. The consumer gets the *real* typeface if the browser has it,
|
|
346
|
-
* at canvas-SDF quality rather than baked-MSDF quality.
|
|
347
|
-
* - `'none'` — hard miss; the run renders nothing.
|
|
348
|
-
*/
|
|
349
|
-
type FontFallbackPolicy = 'substitute' | 'canvas' | 'none';
|
|
350
|
-
declare function setFontFallbackPolicy(next: FontFallbackPolicy): void;
|
|
351
|
-
declare function getFontFallbackPolicy(): FontFallbackPolicy;
|
|
352
|
-
/** Explicit default family for `'substitute'`. When unset, the first
|
|
353
|
-
* registered family wins — the right answer for the common case of an app
|
|
354
|
-
* that registers exactly one. */
|
|
355
|
-
declare function setDefaultFontFamily(family: string): void;
|
|
356
|
-
declare function getDefaultFontFamily(): string | null;
|
|
357
|
-
/** Test helper. Do not call from product code. */
|
|
358
|
-
declare function _resetFallbackForTests(): void;
|
|
1
|
+
import { O as OutlineFontStyle } from './test-seams-DLNsXb8P.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, F as FIXTURE_FONT, e as FontEntry, f as FontFallbackPolicy, g as FontVariant, G as GlyphTextureSink, h as OutlineFace, i as OutlineFontOptions, j as OutlineParser, k as OutlineSource, l as OutlineStatus, m as OutlineVariant, R as RegisteredFont, n as ResolveResult, T as TexSource, o as dynamicPageTextureId, p as ensureFontTexture, q as getDefaultFontFamily, r as getFont, s as getFontFallbackPolicy, t as glyphOutline, u as hasFontOutlines, v as isCanvasFont, w as listCanvasFonts, x as listFontOutlines, y as listFonts, z as markAllFontsNotUploaded, A as outlineStatus, E as parseBmFont, H as registerCanvasFont, I as registerFont, J as registerFontOutlines, K as resetBakeBudget, L as resolveFontVariant, M as resolveGlyphFallback, N as setDefaultFontFamily, P as setFontFallbackPolicy, Q as syncDynamicPageTexture, S as textureCacheKey, U as unregisterCanvasFont, V as unregisterFontOutlines } from './test-seams-DLNsXb8P.js';
|
|
359
3
|
|
|
360
4
|
/**
|
|
361
5
|
* "A glyph the renderer asked for can now paint" — the redraw signal shared
|
|
@@ -397,161 +41,6 @@ declare function glyphGeneration(): number;
|
|
|
397
41
|
*/
|
|
398
42
|
declare function subscribeGlyphReady(cb: () => void): () => void;
|
|
399
43
|
|
|
400
|
-
/**
|
|
401
|
-
* The parsed-font seam for the outline tier.
|
|
402
|
-
*
|
|
403
|
-
* A face answers exactly one question — "what does codepoint N look like?" —
|
|
404
|
-
* and answers it as **SVG path data in em space**: one unit is one em, y
|
|
405
|
-
* grows downward, and the origin sits on the baseline at the glyph's pen
|
|
406
|
-
* position. That is a complete description of the glyph independent of size,
|
|
407
|
-
* zoom, and position, which is the whole reason the tier exists: tessellate
|
|
408
|
-
* once, transform per instance.
|
|
409
|
-
*
|
|
410
|
-
* ### Why `d` and not a command stream
|
|
411
|
-
*
|
|
412
|
-
* `@weasel-js/font` is a Tier A leaf — `@weasel-js/core` depends on it and
|
|
413
|
-
* never the reverse (see `leaf-purity.test.ts`), so this package cannot name
|
|
414
|
-
* core's `PolygonPath`. The alternative to a string would be re-declaring
|
|
415
|
-
* core's `PATH_M`/`PATH_L`/… opcodes over here and trusting two packages to
|
|
416
|
-
* keep the same numbering forever. SVG `d` is the kit's documented language
|
|
417
|
-
* for geometry crossing a boundary (`docs/conventions.md`), core already
|
|
418
|
-
* ships `pathFromD`, and a glyph outline is exactly the case it describes.
|
|
419
|
-
*
|
|
420
|
-
* The string is also a natural cache key and costs nothing to hold: the
|
|
421
|
-
* parse happens once per glyph, and core caches the *tessellation*, not the
|
|
422
|
-
* text.
|
|
423
|
-
*/
|
|
424
|
-
/** Weight/style pair identifying one face within a family. */
|
|
425
|
-
type OutlineFontStyle = 'normal' | 'italic';
|
|
426
|
-
interface OutlineFace {
|
|
427
|
-
/** Font design units per em. Reported for diagnostics; `glyphD` has already
|
|
428
|
-
* divided by it, so callers never need to. */
|
|
429
|
-
unitsPerEm: number;
|
|
430
|
-
/**
|
|
431
|
-
* Em-space SVG path data for `cp`, or `null` when this face has no glyph
|
|
432
|
-
* for the codepoint *or* the glyph has no contours (a space). Both answers
|
|
433
|
-
* mean the same thing to a caller: there is nothing here to tessellate, so
|
|
434
|
-
* fall through to whatever tier would have drawn it.
|
|
435
|
-
*/
|
|
436
|
-
glyphD(cp: number): string | null;
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
|
-
* Turn font file bytes into a face. Async because the default implementation
|
|
440
|
-
* dynamically imports its parser — see `opentypeParser.ts` for why that
|
|
441
|
-
* matters — and because a caller may want to hand back a face that finishes
|
|
442
|
-
* initializing off-thread.
|
|
443
|
-
*/
|
|
444
|
-
type OutlineParser = (bytes: ArrayBuffer) => OutlineFace | Promise<OutlineFace>;
|
|
445
|
-
|
|
446
|
-
/**
|
|
447
|
-
* The outline tier's registry: which faces have real font bytes behind them,
|
|
448
|
-
* and what a given glyph looks like.
|
|
449
|
-
*
|
|
450
|
-
* ### What this tier is for
|
|
451
|
-
*
|
|
452
|
-
* A distance field is a *sampled* representation. The baked MSDF atlas is
|
|
453
|
-
* generated from outlines and holds up well; the dynamic canvas-SDF tier
|
|
454
|
-
* (`../dynamic/`) reconstructs its field from a 48px raster, so magnifying it
|
|
455
|
-
* past a few times the bake size exposes the raster as contour wobble — bumps
|
|
456
|
-
* one bake-texel apart, ±2–3px at 8×. No bake size fixes that, it only moves
|
|
457
|
-
* the size at which it appears (`glyphRasterizer.ts` measures the tradeoff:
|
|
458
|
-
* error is *minimized at* the bake size, so raising it spoils the 12–32px
|
|
459
|
-
* range where most text lives).
|
|
460
|
-
*
|
|
461
|
-
* Above a size threshold the answer is to stop sampling: parse the font, take
|
|
462
|
-
* the glyph's outline, and hand it to the path renderer the kit already has.
|
|
463
|
-
* Exact at every zoom, and — because a glyph becomes an ordinary path — it
|
|
464
|
-
* takes strokes and non-solid fills for free.
|
|
465
|
-
*
|
|
466
|
-
* Below the threshold the atlas still wins, and not narrowly: outlines carry
|
|
467
|
-
* no hinting and no stem darkening, so body text at 12–16px rendered from
|
|
468
|
-
* them looks *worse* than a platform rasterizer that puts stems on the pixel
|
|
469
|
-
* grid. Two tiers with a size threshold is the standard hybrid, not a
|
|
470
|
-
* compromise.
|
|
471
|
-
*
|
|
472
|
-
* ### Metric neutrality (the load-bearing invariant)
|
|
473
|
-
*
|
|
474
|
-
* This tier replaces how a glyph is *painted*, never where it sits. Advances,
|
|
475
|
-
* kerning, line breaking and baselines all keep coming from whichever tier
|
|
476
|
-
* resolved the run — the baked atlas or the canvas face. That is what lets
|
|
477
|
-
* the threshold be a rendering decision: zooming past it swaps glyph geometry
|
|
478
|
-
* with the layout untouched, so text cannot reflow under the user's cursor,
|
|
479
|
-
* and `measureTextBounds` / `textLineBoxes` need to know nothing about
|
|
480
|
-
* outlines at all.
|
|
481
|
-
*
|
|
482
|
-
* The cost is that a face whose real advances disagree with the atlas's
|
|
483
|
-
* inherits the atlas's. For the bundled Inter that is exact (the subset ships
|
|
484
|
-
* from the same source the atlas was baked from), and for a machine font the
|
|
485
|
-
* outline bytes and the canvas metrics come from the same file.
|
|
486
|
-
*
|
|
487
|
-
* ### Availability
|
|
488
|
-
*
|
|
489
|
-
* Everything here degrades rather than blocks. Bytes arrive over `fetch` or
|
|
490
|
-
* `queryLocalFonts` — asynchronous, and in the local case gated behind a
|
|
491
|
-
* permission the user can refuse — so `glyphOutline` answers `null` until a
|
|
492
|
-
* face is loaded and forever if it failed, and the caller falls back down the
|
|
493
|
-
* ladder to SDF. A family must never render *nothing* because the outline
|
|
494
|
-
* tier could not get bytes.
|
|
495
|
-
*/
|
|
496
|
-
|
|
497
|
-
/**
|
|
498
|
-
* Where a face's bytes come from. A URL is fetched; a buffer or blob is used
|
|
499
|
-
* as-is; a thunk is called at first use, which is what lets `queryLocalFonts`
|
|
500
|
-
* results be registered eagerly and read lazily — the permission prompt and
|
|
501
|
-
* the (potentially many megabytes of) blob only happen for a family the
|
|
502
|
-
* document actually sets text in.
|
|
503
|
-
*/
|
|
504
|
-
type OutlineSource = string | ArrayBuffer | Blob | (() => ArrayBuffer | Blob | Promise<ArrayBuffer | Blob>);
|
|
505
|
-
interface OutlineVariant {
|
|
506
|
-
weight?: number;
|
|
507
|
-
style?: OutlineFontStyle;
|
|
508
|
-
}
|
|
509
|
-
interface OutlineFontOptions {
|
|
510
|
-
/** Override the default opentype.js parser. Mostly a test seam; also the
|
|
511
|
-
* hook for a consumer who already has a font parser in their bundle. */
|
|
512
|
-
parser?: OutlineParser;
|
|
513
|
-
}
|
|
514
|
-
/** Load state of one registered face. */
|
|
515
|
-
type OutlineStatus = 'idle' | 'loading' | 'ready' | 'failed';
|
|
516
|
-
/**
|
|
517
|
-
* Register font file bytes for one face, so text set in it can render from
|
|
518
|
-
* outlines above the size threshold.
|
|
519
|
-
*
|
|
520
|
-
* Registration is cheap and synchronous: nothing is fetched, and the parser
|
|
521
|
-
* is not even imported, until a frame actually asks for a glyph from this
|
|
522
|
-
* face. Registering a face that is never drawn large costs one map entry.
|
|
523
|
-
*
|
|
524
|
-
* The variant must match exactly what the layout tier resolved — this is
|
|
525
|
-
* deliberately *not* a fallback chain. Painting 400-weight outlines at
|
|
526
|
-
* 700-weight advances, or upright outlines where the shader was going to
|
|
527
|
-
* fake an oblique, looks worse than the SDF it replaced; a miss here falls
|
|
528
|
-
* back down the ladder instead, which is always safe.
|
|
529
|
-
*/
|
|
530
|
-
declare function registerFontOutlines(family: string, variant: OutlineVariant, source: OutlineSource, opts?: OutlineFontOptions): void;
|
|
531
|
-
/** Drop a registration. Glyphs already handed out stay valid — they are
|
|
532
|
-
* plain path data — but nothing further resolves from this face. */
|
|
533
|
-
declare function unregisterFontOutlines(family: string, variant?: OutlineVariant): void;
|
|
534
|
-
/**
|
|
535
|
-
* Is an outline face registered for this exact variant? Answers `true` while
|
|
536
|
-
* the bytes are still loading — the question is "could this face serve
|
|
537
|
-
* outlines", which is what a caller deciding whether to *ask* wants; use
|
|
538
|
-
* `outlineStatus` for the narrower "can it right now".
|
|
539
|
-
*/
|
|
540
|
-
declare function hasFontOutlines(family: string, weight?: number, style?: OutlineFontStyle): boolean;
|
|
541
|
-
/** Load state of a registered face; `null` when nothing is registered. */
|
|
542
|
-
declare function outlineStatus(family: string, weight?: number, style?: OutlineFontStyle): OutlineStatus | null;
|
|
543
|
-
/** Every registered outline face and its load state — the enumeration a
|
|
544
|
-
* debug overlay or font picker needs, mirroring `listFonts`. */
|
|
545
|
-
declare function listFontOutlines(): readonly {
|
|
546
|
-
family: string;
|
|
547
|
-
weight: number;
|
|
548
|
-
style: OutlineFontStyle;
|
|
549
|
-
status: OutlineStatus;
|
|
550
|
-
}[];
|
|
551
|
-
declare function glyphOutline(family: string, weight: number, style: OutlineFontStyle, cp: number): string | null;
|
|
552
|
-
/** @internal Test seam — registry and warn-once keys are module state. */
|
|
553
|
-
declare function _resetFontOutlinesForTests(): void;
|
|
554
|
-
|
|
555
44
|
/**
|
|
556
45
|
* Local Font Access → outline registrations.
|
|
557
46
|
*
|
|
@@ -591,6 +80,7 @@ declare function parseFontStyle(style: string): {
|
|
|
591
80
|
weight: number;
|
|
592
81
|
style: OutlineFontStyle;
|
|
593
82
|
};
|
|
83
|
+
/** Options for `registerLocalFontOutlines`. */
|
|
594
84
|
interface LocalFontOutlinesOptions {
|
|
595
85
|
/**
|
|
596
86
|
* Restrict registration to these families. Defaults to every installed
|
|
@@ -600,6 +90,7 @@ interface LocalFontOutlinesOptions {
|
|
|
600
90
|
*/
|
|
601
91
|
families?: readonly string[];
|
|
602
92
|
}
|
|
93
|
+
/** What `registerLocalFontOutlines` registered. */
|
|
603
94
|
interface LocalFontOutlinesResult {
|
|
604
95
|
/** Families that now have at least one outline face, in menu order. */
|
|
605
96
|
families: readonly string[];
|
|
@@ -658,6 +149,8 @@ declare function enableLocalFontOutlines(opts?: LocalFontOutlinesOptions): Promi
|
|
|
658
149
|
* zero-width band, which would be the aliased behavior all over again.
|
|
659
150
|
*/
|
|
660
151
|
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";
|
|
152
|
+
/** Fragment shader for the MSDF text program. Samples the atlas, applies the
|
|
153
|
+
* color transform, and emits premultiplied alpha. */
|
|
661
154
|
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";
|
|
662
155
|
/**
|
|
663
156
|
* Single-channel sibling of TEXT_FRAG_SRC for runtime canvas-SDF glyphs
|
|
@@ -670,7 +163,11 @@ declare const TEXT_FRAG_SRC = "#version 300 es\nprecision highp float;\nin vec2
|
|
|
670
163
|
* larger bake nor extra taps would improve the small-text end.
|
|
671
164
|
*/
|
|
672
165
|
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";
|
|
166
|
+
/** Uniform names the text program declares, for the caller that looks up and
|
|
167
|
+
* caches their locations. */
|
|
673
168
|
declare const TEXT_SDF_UNIFORMS: readonly ["u_proj", "u_model", "u_atlas", "u_color", "u_alpha", "u_synthBold", "u_synthItalic", "u_colorMatrix", "u_colorBias"];
|
|
169
|
+
/** Vertex attribute names the text program declares, in the order the
|
|
170
|
+
* interleaved buffer packs them. */
|
|
674
171
|
declare const TEXT_SDF_ATTRIBUTES: readonly ["a_position", "a_uv", "a_baselineY"];
|
|
675
172
|
|
|
676
|
-
export { type
|
|
173
|
+
export { type LocalFontOutlinesOptions, type LocalFontOutlinesResult, OutlineFontStyle, TEXT_FRAG_R8_SRC, TEXT_FRAG_SRC, TEXT_SDF_ATTRIBUTES, TEXT_SDF_UNIFORMS, TEXT_VERT_SRC, canQueryLocalFonts, enableLocalFontOutlines, glyphGeneration, parseFontStyle, subscribeGlyphReady };
|