@weasel-js/font 1.4.3 → 1.5.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/README.md +1 -1
- package/dist/index.d.ts +50 -58
- package/dist/index.js +10 -88
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ no glyphs at all.
|
|
|
15
15
|
| `registerFont` | The registry, variant resolution, texture upload |
|
|
16
16
|
| `dynamic/` | Runtime canvas-SDF rasterization for glyphs with no baked atlas |
|
|
17
17
|
| `textureSink` | The `GlyphTextureSink` seam — the renderer injects GL texture upload, so this package never imports one |
|
|
18
|
-
| `textSdf` |
|
|
18
|
+
| `textSdf` | The GLSL that turns an atlas sample into glyph coverage, for the renderer's batch program to paste in |
|
|
19
19
|
|
|
20
20
|
## Fallback
|
|
21
21
|
|
package/dist/index.d.ts
CHANGED
|
@@ -109,65 +109,57 @@ interface LocalFontOutlinesResult {
|
|
|
109
109
|
declare function enableLocalFontOutlines(opts?: LocalFontOutlinesOptions): Promise<LocalFontOutlinesResult>;
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
|
-
* GLSL
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* MSDF channel layout: msdf-bmfont-xml outputs R,G,B channels as independent
|
|
132
|
-
* signed-distance fields covering different edge directions. The true SDF
|
|
133
|
-
* value is the median of R,G,B; this recovers sharp outlines while averaging
|
|
134
|
-
* out single-channel aliasing artifacts.
|
|
135
|
-
*
|
|
136
|
-
* Antialiasing (`aaWidth`, both shaders): the smoothstep band must be one
|
|
137
|
-
* *screen* pixel wide, so it is derived per-fragment from `fwidth(sdfVal)` —
|
|
138
|
-
* the rate the field changes between adjacent fragments. That single quantity
|
|
139
|
-
* already folds in font size, zoom, and DPR: minify the glyph and the field
|
|
140
|
-
* changes faster, so the band widens in field units to stay one pixel on
|
|
141
|
-
* screen; magnify it and the band narrows.
|
|
142
|
-
*
|
|
143
|
-
* A *constant* band cannot be correct at more than one scale, and this shader
|
|
144
|
-
* used one (0.05) until 2026-07-29. At 16px text the band collapsed to well
|
|
145
|
-
* under a pixel and glyph edges quantized to hard stair-steps; at display
|
|
146
|
-
* sizes the same constant read mushy. `fwidth` is core in GLSL ES 3.00, so
|
|
147
|
-
* no extension guard is needed. The `max()` floor keeps a degenerate
|
|
148
|
-
* derivative (flat field, or a driver returning 0) from producing a
|
|
149
|
-
* zero-width band, which would be the aliased behavior all over again.
|
|
112
|
+
* The GLSL that turns an atlas sample into glyph coverage, and the constants
|
|
113
|
+
* naming which kind of atlas a sample came from.
|
|
114
|
+
*
|
|
115
|
+
* Not a program: text has no program of its own any more. Glyphs stage into
|
|
116
|
+
* the renderer's batch alongside solid geometry and image quads, and the batch
|
|
117
|
+
* shader pastes this in — which is why what lives here is a snippet rather
|
|
118
|
+
* than a vertex and fragment pair. The two channel layouts a glyph atlas can
|
|
119
|
+
* have are the part that belongs to this package, so they stay here.
|
|
120
|
+
*
|
|
121
|
+
* MSDF channel layout: msdf-bmfont-xml writes R, G and B as independent
|
|
122
|
+
* signed-distance fields covering different edge directions, and the true
|
|
123
|
+
* field is their median — which recovers a sharp outline while averaging out
|
|
124
|
+
* single-channel aliasing. The runtime canvas bake writes one channel that
|
|
125
|
+
* *is* the field. Both encode the edge at 0.5, which is what lets one
|
|
126
|
+
* threshold serve them.
|
|
127
|
+
*
|
|
128
|
+
* The single-channel bake rounds corners away from its bake size, mildest near
|
|
129
|
+
* it. `glyphRasterizer.ts` carries the measurements and the reason neither a
|
|
130
|
+
* larger bake nor extra taps would improve the small-text end.
|
|
150
131
|
*/
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
declare const
|
|
132
|
+
/** `a_paintMode` value for glyphs off an MSDF atlas — the median of R,G,B. */
|
|
133
|
+
declare const GLYPH_MODE_MSDF = 1;
|
|
134
|
+
/** `a_paintMode` value for glyphs off a runtime canvas bake — `.r` alone. */
|
|
135
|
+
declare const GLYPH_MODE_R8 = 2;
|
|
155
136
|
/**
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
137
|
+
* Glyph coverage from one atlas sample, as GLSL for a program to paste in.
|
|
138
|
+
*
|
|
139
|
+
* `mode` says which field the sample carries: `GLYPH_MODE_MSDF` takes the
|
|
140
|
+
* median of R,G,B, `GLYPH_MODE_R8` reads `.r` alone. A caller whose fragment
|
|
141
|
+
* is not a glyph at all still calls this and discards the result — see below.
|
|
142
|
+
*
|
|
143
|
+
* The antialiasing band has to be one *screen* pixel wide, so it comes from
|
|
144
|
+
* `fwidth` of the field rather than from a constant: that single quantity
|
|
145
|
+
* folds in font size, zoom and DPR at once. Minify the glyph and the field
|
|
146
|
+
* changes faster between neighboring fragments, so the band widens in field
|
|
147
|
+
* units to stay one pixel on screen; magnify it and the band narrows. A
|
|
148
|
+
* constant band cannot be right at more than one scale, and this was one
|
|
149
|
+
* (0.05) until 2026-07-29 — at 16px it fell well under a pixel and edges
|
|
150
|
+
* quantized to stair-steps, while display sizes read mushy. The `max()` floor
|
|
151
|
+
* keeps a degenerate derivative — a flat field, or a driver answering 0 — from
|
|
152
|
+
* collapsing the band back to that.
|
|
153
|
+
*
|
|
154
|
+
* **Nothing here branches, and the caller must not branch around it.**
|
|
155
|
+
* `fwidth` in non-uniform control flow is undefined, so the derivative has to
|
|
156
|
+
* be taken before anything selects on paint mode. That is why a merged program
|
|
157
|
+
* runs the glyph math on fragments that are not glyphs, and it is why the two
|
|
158
|
+
* fields are selected with a `mix` rather than an `if`.
|
|
159
|
+
*
|
|
160
|
+
* `synthBold` shifts the threshold to thicken strokes where the resolver fell
|
|
161
|
+
* back from a missing bold variant to the regular atlas.
|
|
164
162
|
*/
|
|
165
|
-
declare const
|
|
166
|
-
/** Uniform names the text program declares, for the caller that looks up and
|
|
167
|
-
* caches their locations. */
|
|
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. */
|
|
171
|
-
declare const TEXT_SDF_ATTRIBUTES: readonly ["a_position", "a_uv", "a_baselineY"];
|
|
163
|
+
declare const GLYPH_COVERAGE_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) {\n float field = mix(median(texel.r, texel.g, texel.b), texel.r, step(1.5, mode));\n float aaW = max(0.5 * fwidth(field), 0.0005);\n float threshold = 0.5 - synthBold;\n return smoothstep(threshold - aaW, threshold + aaW, field);\n}\n";
|
|
172
164
|
|
|
173
|
-
export {
|
|
165
|
+
export { GLYPH_COVERAGE_GLSL, GLYPH_MODE_MSDF, GLYPH_MODE_R8, type LocalFontOutlinesOptions, type LocalFontOutlinesResult, OutlineFontStyle, canQueryLocalFonts, enableLocalFontOutlines, glyphGeneration, parseFontStyle, subscribeGlyphReady };
|
package/dist/index.js
CHANGED
|
@@ -71,102 +71,24 @@ async function enableLocalFontOutlines(opts = {}) {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
// src/textSdf.ts
|
|
74
|
-
var
|
|
74
|
+
var GLYPH_MODE_MSDF = 1;
|
|
75
|
+
var GLYPH_MODE_R8 = 2;
|
|
76
|
+
var GLYPH_COVERAGE_GLSL = (
|
|
75
77
|
/* glsl */
|
|
76
|
-
|
|
77
|
-
in vec2 a_position;
|
|
78
|
-
in vec2 a_uv;
|
|
79
|
-
in float a_baselineY;
|
|
80
|
-
uniform mat3 u_proj;
|
|
81
|
-
uniform mat3 u_model;
|
|
82
|
-
uniform float u_synthItalic;
|
|
83
|
-
out vec2 v_uv;
|
|
84
|
-
void main() {
|
|
85
|
-
// Synthetic italic: shift x by (a_baselineY - a_position.y) * tan(angle).
|
|
86
|
-
// Above-baseline vertices (lower y in screen coords) lean further right.
|
|
87
|
-
vec2 skewed = vec2(
|
|
88
|
-
a_position.x + (a_baselineY - a_position.y) * tan(u_synthItalic),
|
|
89
|
-
a_position.y
|
|
90
|
-
);
|
|
91
|
-
vec3 screen = u_model * vec3(skewed, 1.0);
|
|
92
|
-
vec3 clip = u_proj * vec3(screen.xy, 1.0);
|
|
93
|
-
gl_Position = vec4(clip.xy, 0.0, 1.0);
|
|
94
|
-
v_uv = a_uv;
|
|
95
|
-
}
|
|
96
|
-
`
|
|
97
|
-
);
|
|
98
|
-
var TEXT_FRAG_SRC = (
|
|
99
|
-
/* glsl */
|
|
100
|
-
`#version 300 es
|
|
101
|
-
precision highp float;
|
|
102
|
-
in vec2 v_uv;
|
|
103
|
-
uniform sampler2D u_atlas;
|
|
104
|
-
uniform vec4 u_color;
|
|
105
|
-
uniform float u_alpha;
|
|
106
|
-
uniform float u_synthBold;
|
|
107
|
-
uniform mat4 u_colorMatrix;
|
|
108
|
-
uniform vec4 u_colorBias;
|
|
109
|
-
out vec4 outColor;
|
|
110
|
-
|
|
78
|
+
`
|
|
111
79
|
float median(float r, float g, float b) {
|
|
112
80
|
return max(min(r, g), min(max(r, g), b));
|
|
113
81
|
}
|
|
114
82
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
float
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// u_synthBold shifts the SDF threshold to thicken strokes when the
|
|
121
|
-
// resolver fell back from a missing bold variant to the regular atlas.
|
|
122
|
-
float threshold = 0.5 - u_synthBold;
|
|
123
|
-
float msdfAlpha = smoothstep(threshold - aaW, threshold + aaW, sdfVal);
|
|
124
|
-
vec4 src = vec4(u_color.rgb, u_color.a);
|
|
125
|
-
vec4 mapped = clamp(u_colorMatrix * src + u_colorBias, 0.0, 1.0);
|
|
126
|
-
float a = mapped.a * msdfAlpha * u_alpha;
|
|
127
|
-
outColor = vec4(mapped.rgb * a, a);
|
|
83
|
+
float glyphCoverage(vec4 texel, float mode, float synthBold) {
|
|
84
|
+
float field = mix(median(texel.r, texel.g, texel.b), texel.r, step(1.5, mode));
|
|
85
|
+
float aaW = max(0.5 * fwidth(field), 0.0005);
|
|
86
|
+
float threshold = 0.5 - synthBold;
|
|
87
|
+
return smoothstep(threshold - aaW, threshold + aaW, field);
|
|
128
88
|
}
|
|
129
89
|
`
|
|
130
90
|
);
|
|
131
|
-
var TEXT_FRAG_R8_SRC = (
|
|
132
|
-
/* glsl */
|
|
133
|
-
`#version 300 es
|
|
134
|
-
precision highp float;
|
|
135
|
-
in vec2 v_uv;
|
|
136
|
-
uniform sampler2D u_atlas;
|
|
137
|
-
uniform vec4 u_color;
|
|
138
|
-
uniform float u_alpha;
|
|
139
|
-
uniform float u_synthBold;
|
|
140
|
-
uniform mat4 u_colorMatrix;
|
|
141
|
-
uniform vec4 u_colorBias;
|
|
142
|
-
out vec4 outColor;
|
|
143
|
-
|
|
144
|
-
void main() {
|
|
145
|
-
float sdfVal = texture(u_atlas, v_uv).r;
|
|
146
|
-
// Screen-space AA band \u2014 see the file header. Half of fwidth spans ~1px.
|
|
147
|
-
float aaW = max(0.5 * fwidth(sdfVal), 0.0005);
|
|
148
|
-
float threshold = 0.5 - u_synthBold;
|
|
149
|
-
float sdfAlpha = smoothstep(threshold - aaW, threshold + aaW, sdfVal);
|
|
150
|
-
vec4 src = vec4(u_color.rgb, u_color.a);
|
|
151
|
-
vec4 mapped = clamp(u_colorMatrix * src + u_colorBias, 0.0, 1.0);
|
|
152
|
-
float a = mapped.a * sdfAlpha * u_alpha;
|
|
153
|
-
outColor = vec4(mapped.rgb * a, a);
|
|
154
|
-
}
|
|
155
|
-
`
|
|
156
|
-
);
|
|
157
|
-
var TEXT_SDF_UNIFORMS = [
|
|
158
|
-
"u_proj",
|
|
159
|
-
"u_model",
|
|
160
|
-
"u_atlas",
|
|
161
|
-
"u_color",
|
|
162
|
-
"u_alpha",
|
|
163
|
-
"u_synthBold",
|
|
164
|
-
"u_synthItalic",
|
|
165
|
-
"u_colorMatrix",
|
|
166
|
-
"u_colorBias"
|
|
167
|
-
];
|
|
168
|
-
var TEXT_SDF_ATTRIBUTES = ["a_position", "a_uv", "a_baselineY"];
|
|
169
91
|
|
|
170
|
-
export {
|
|
92
|
+
export { GLYPH_COVERAGE_GLSL, GLYPH_MODE_MSDF, GLYPH_MODE_R8, canQueryLocalFonts, enableLocalFontOutlines, parseFontStyle };
|
|
171
93
|
//# sourceMappingURL=index.js.map
|
|
172
94
|
//# 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 { OutlineFontStyle } from './OutlineFace';\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/**\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: OutlineFontStyle } {\n const s = style.toLowerCase();\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 = style.toLowerCase().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 });\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 * GLSL ES 3.0 sources for the built-in MSDF text shader.\n *\n * Vertex inputs (interleaved, stride 20 bytes = 5 × float):\n * a_position vec2 screen-space x,y of the glyph quad vertex\n * a_uv vec2 atlas UV (0..1)\n * a_baselineY float line baseline Y in screen space (for synth-italic skew)\n *\n * Uniforms:\n * u_proj mat3 screen → clip projection\n * u_model mat3 cumulative group transform\n * u_atlas sampler2D the MSDF atlas texture (bound to TEXTURE0)\n * u_color vec4 text color (straight RGBA)\n * u_alpha float group alpha multiplier\n * u_colorMatrix mat4 color transform applied to u_color before alpha modulation\n * u_colorBias vec4 bias added after the matrix (identity = zero bias)\n *\n * Output: PREMULTIPLIED alpha — `vec4(color.rgb * a, a)` per conventions §2.\n * Blend func: ONE / ONE_MINUS_SRC_ALPHA.\n *\n * MSDF channel layout: msdf-bmfont-xml outputs R,G,B channels as independent\n * signed-distance fields covering different edge directions. The true SDF\n * value is the median of R,G,B; this recovers sharp outlines while averaging\n * out single-channel aliasing artifacts.\n *\n * Antialiasing (`aaWidth`, both shaders): the smoothstep band must be one\n * *screen* pixel wide, so it is derived per-fragment from `fwidth(sdfVal)` —\n * the rate the field changes between adjacent fragments. That single quantity\n * already folds in font size, zoom, and DPR: minify the glyph and the field\n * changes faster, so the band widens in field units to stay one pixel on\n * screen; magnify it and the band narrows.\n *\n * A *constant* band cannot be correct at more than one scale, and this shader\n * used one (0.05) until 2026-07-29. At 16px text the band collapsed to well\n * under a pixel and glyph edges quantized to hard stair-steps; at display\n * sizes the same constant read mushy. `fwidth` is core in GLSL ES 3.00, so\n * no extension guard is needed. The `max()` floor keeps a degenerate\n * derivative (flat field, or a driver returning 0) from producing a\n * zero-width band, which would be the aliased behavior all over again.\n */\n\nexport const TEXT_VERT_SRC = /* glsl */ `#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`;\n\n/** Fragment shader for the MSDF text program. Samples the atlas, applies the\n * color transform, and emits premultiplied alpha. */\nexport const TEXT_FRAG_SRC = /* glsl */ `#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 — 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`;\n\n/**\n * Single-channel sibling of TEXT_FRAG_SRC for runtime canvas-SDF glyphs\n * (DynamicGlyphAtlas R8 pages): the R channel IS the distance field, so no\n * median. Threshold semantics (0.5 edge, u_synthBold shift) match the MSDF\n * shader because the bake encodes the edge at ~128.\n *\n * Accepted trade: corner rounding away from the bake size, mildest near it.\n * `glyphRasterizer.ts` carries the measurements and the reason neither a\n * larger bake nor extra taps would improve the small-text end.\n */\nexport const TEXT_FRAG_R8_SRC = /* glsl */ `#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 — 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`;\n\n/** Uniform names the text program declares, for the caller that looks up and\n * caches their locations. */\nexport const TEXT_SDF_UNIFORMS = [\n 'u_proj', 'u_model', 'u_atlas', 'u_color', 'u_alpha',\n 'u_synthBold', 'u_synthItalic', 'u_colorMatrix', 'u_colorBias',\n] as const;\n\n/** Vertex attribute names the text program declares, in the order the\n * interleaved buffer packs them. */\nexport const TEXT_SDF_ATTRIBUTES = ['a_position', 'a_uv', 'a_baselineY'] as const;\n"]}
|
|
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 { OutlineFontStyle } from './OutlineFace';\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/**\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: OutlineFontStyle } {\n const s = style.toLowerCase();\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 = style.toLowerCase().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 });\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\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 comes from\n * `fwidth` of the field rather than from a constant: that single quantity\n * folds in font size, zoom and DPR at once. Minify the glyph and the field\n * changes faster between neighboring fragments, so the band widens in field\n * units to stay one pixel on screen; magnify it and the band narrows. A\n * constant band cannot be right at more than one scale, and this was one\n * (0.05) until 2026-07-29 — at 16px it fell well under a pixel and edges\n * quantized to stair-steps, while display sizes read mushy. The `max()` floor\n * keeps a degenerate derivative — a flat field, or a driver answering 0 — from\n * collapsing the band back to that.\n *\n * **Nothing here branches, and the caller must not branch around it.**\n * `fwidth` in non-uniform control flow is undefined, so the derivative 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) {\n float field = mix(median(texel.r, texel.g, texel.b), texel.r, step(1.5, mode));\n float aaW = max(0.5 * fwidth(field), 0.0005);\n float threshold = 0.5 - synthBold;\n return smoothstep(threshold - aaW, threshold + aaW, field);\n}\n`;\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weasel-js/font",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "MSDF font atlases, glyph metrics, and runtime glyph rasterization for @weasel-js/core. Registry, kerning-aware glyph layout, and the SDF text shader source.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|