vgpu-render 0.1.0 → 0.2.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 +76 -26
- package/dist/curve.d.ts +1 -1
- package/dist/debug-draw-3d.d.ts +3 -3
- package/dist/debug-draw-3d.js +10 -10
- package/dist/{frame-recorder-2d.d.ts → frame-draw-list-2d.d.ts} +11 -11
- package/dist/{frame-recorder-2d.js → frame-draw-list-2d.js} +6 -6
- package/dist/{frame-recorder.d.ts → frame-draw-list.d.ts} +60 -8
- package/dist/{frame-recorder.js → frame-draw-list.js} +51 -10
- package/dist/geometry-generators.d.ts +2 -2
- package/dist/geometry-generators.js +1 -1
- package/dist/gpu-debug-draw-2d.d.ts +1 -1
- package/dist/gpu-debug-draw-3d.d.ts +1 -1
- package/dist/gpu-debug-draw-3d.js +4 -2
- package/dist/gpu-text-3d.d.ts +1 -1
- package/dist/gpu-text-3d.js +49 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -3
- package/dist/mesh-batch-3d.d.ts +1 -1
- package/dist/shape2d-shader.js +3 -2
- package/dist/text-atlas.d.ts +6 -4
- package/dist/text-atlas.js +9 -6
- package/dist/text-font.d.ts +48 -7
- package/dist/text-font.js +63 -11
- package/dist/text-layout.d.ts +63 -4
- package/dist/text-layout.js +58 -8
- package/dist/text-msdf-baked.d.ts +9 -4
- package/dist/text-msdf-baked.js +9 -4
- package/dist/text-msdf-font.d.ts +10 -4
- package/dist/text-msdf-font.js +9 -3
- package/dist/vector-math.d.ts +19 -0
- package/dist/vector-math.js +37 -0
- package/package.json +1 -1
- package/dist/layout-3d.d.ts +0 -227
- package/dist/layout-3d.js +0 -226
- package/dist/scene-examples.d.ts +0 -39
- package/dist/scene-examples.js +0 -200
- package/dist/scenes-2d.d.ts +0 -18
- package/dist/scenes-2d.js +0 -160
- package/dist/scenes-3d.d.ts +0 -46
- package/dist/scenes-3d.js +0 -1994
- package/dist/text-scenes.d.ts +0 -85
- package/dist/text-scenes.js +0 -338
package/dist/text-font.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { FIRA_MONO_MEDIUM_SUBSET_BASE64, FIRA_MONO_MEDIUM_SUBSET_TTF_BASE64 } from "./assets/fira-mono-medium-subset.js";
|
|
17
17
|
import { coverageToSdf, DEFAULT_SDF_RANGE } from "./text-sdf.js";
|
|
18
|
+
import { fontFaceKey } from "./text-layout.js";
|
|
18
19
|
/**
|
|
19
20
|
* Family name the bundled font is registered under. It is deliberately not `"Fira Mono"` so
|
|
20
21
|
* a locally installed face can never shadow the inlined one.
|
|
@@ -36,6 +37,38 @@ export function decodeBase64Bytes(base64) {
|
|
|
36
37
|
bytes[index] = binary.charCodeAt(index);
|
|
37
38
|
return bytes;
|
|
38
39
|
}
|
|
40
|
+
const registeredFamilies = new Map();
|
|
41
|
+
/**
|
|
42
|
+
* Registers a family whose weights are separate faces, each carrying its own `weight` descriptor.
|
|
43
|
+
*
|
|
44
|
+
* This is the only way to get more than one drawn weight out of Canvas2D: the font shorthand carries a
|
|
45
|
+
* single synthesised bold step, and `font-variation-settings` does not exist there, but the browser
|
|
46
|
+
* does select a face by the weight it was registered with. Registering the same family once per
|
|
47
|
+
* weight therefore turns `weight: 700` into a real bold face instead of an emboldened regular one.
|
|
48
|
+
*
|
|
49
|
+
* Idempotent per family, like `loadDefaultFont`, and browser-only.
|
|
50
|
+
*/
|
|
51
|
+
export function loadFontFaces(family, faces) {
|
|
52
|
+
const existing = registeredFamilies.get(family);
|
|
53
|
+
if (existing !== undefined)
|
|
54
|
+
return existing;
|
|
55
|
+
const registration = registerFontFaces(family, faces);
|
|
56
|
+
registeredFamilies.set(family, registration);
|
|
57
|
+
return registration;
|
|
58
|
+
}
|
|
59
|
+
async function registerFontFaces(family, faces) {
|
|
60
|
+
const fonts = typeof document === "undefined" ? undefined : document.fonts;
|
|
61
|
+
if (fonts === undefined || typeof FontFace === "undefined") {
|
|
62
|
+
throw new Error("registering a font family requires a browser environment");
|
|
63
|
+
}
|
|
64
|
+
const loaded = faces.map(async (face) => {
|
|
65
|
+
const definition = new FontFace(family, decodeBase64Bytes(face.woff2Base64), { weight: String(face.weight) });
|
|
66
|
+
await definition.load();
|
|
67
|
+
return definition;
|
|
68
|
+
});
|
|
69
|
+
for (const definition of await Promise.all(loaded))
|
|
70
|
+
fonts.add(definition);
|
|
71
|
+
}
|
|
39
72
|
/** Decodes the inlined WOFF2 bytes of the bundled font, which is what `FontFace` takes. */
|
|
40
73
|
export function decodeEmbeddedFont() {
|
|
41
74
|
return decodeBase64Bytes(FIRA_MONO_MEDIUM_SUBSET_BASE64);
|
|
@@ -66,6 +99,25 @@ async function registerDefaultFont() {
|
|
|
66
99
|
await face.load();
|
|
67
100
|
fonts.add(face);
|
|
68
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* The CSS font shorthand that selects `face` at `fontSize`.
|
|
104
|
+
*
|
|
105
|
+
* Order follows the shorthand's grammar — style, weight, stretch, size, family — because Canvas2D
|
|
106
|
+
* parses the value as one. Two of the axes are conditional:
|
|
107
|
+
*
|
|
108
|
+
* - `font-stretch` keywords are only kept by the parser when the face has a `wdth` axis. A static
|
|
109
|
+
* face drops the keyword and draws every stop identically, which is why the bundled subset cannot
|
|
110
|
+
* show a `FontWidth` difference at all.
|
|
111
|
+
* - `italic` is the only slant Canvas2D keeps; `oblique <angle>` reads back as plain `italic`.
|
|
112
|
+
*
|
|
113
|
+
* A weight the loaded face does not carry is synthesised by the browser (one emboldened step, which
|
|
114
|
+
* keeps the advance), so `weight` at least changes what is drawn for the bundled static face.
|
|
115
|
+
*/
|
|
116
|
+
export function canvasFontString(face, fontSize) {
|
|
117
|
+
const style = face.style === "italic" ? "italic " : "";
|
|
118
|
+
const stretch = face.width === "normal" ? "" : `${face.width} `;
|
|
119
|
+
return `${style}${face.weight} ${stretch}${fontSize}px "${face.family}"`;
|
|
120
|
+
}
|
|
69
121
|
/**
|
|
70
122
|
* Canvas2D glyph source, doubling as the layout `TextMeasurer` so the canvas is only set up
|
|
71
123
|
* once per canvas-configured face and every `measureText` result is cached.
|
|
@@ -109,16 +161,16 @@ export class CanvasGlyphRasterizer {
|
|
|
109
161
|
}
|
|
110
162
|
return this.#surface;
|
|
111
163
|
}
|
|
112
|
-
#font(
|
|
113
|
-
return
|
|
164
|
+
#font(face, fontSize) {
|
|
165
|
+
return canvasFontString(face, fontSize);
|
|
114
166
|
}
|
|
115
|
-
verticalMetrics(
|
|
116
|
-
const key = `${
|
|
167
|
+
verticalMetrics(face, fontSize) {
|
|
168
|
+
const key = `${fontFaceKey(face)}|${fontSize}`;
|
|
117
169
|
const cached = this.#vertical.get(key);
|
|
118
170
|
if (cached !== undefined)
|
|
119
171
|
return cached;
|
|
120
172
|
const context = this.#ensureSurface().context;
|
|
121
|
-
context.font = this.#font(
|
|
173
|
+
context.font = this.#font(face, fontSize);
|
|
122
174
|
const metrics = context.measureText("M");
|
|
123
175
|
const ascent = metrics.fontBoundingBoxAscent > 0 ? metrics.fontBoundingBoxAscent : fontSize * FALLBACK_ASCENT_RATIO;
|
|
124
176
|
const descent = metrics.fontBoundingBoxDescent > 0 ? metrics.fontBoundingBoxDescent : fontSize * FALLBACK_DESCENT_RATIO;
|
|
@@ -126,13 +178,13 @@ export class CanvasGlyphRasterizer {
|
|
|
126
178
|
this.#vertical.set(key, value);
|
|
127
179
|
return value;
|
|
128
180
|
}
|
|
129
|
-
glyphMetrics(character,
|
|
130
|
-
const key = `${
|
|
181
|
+
glyphMetrics(character, face, fontSize) {
|
|
182
|
+
const key = `${fontFaceKey(face)}|${fontSize}|${character}`;
|
|
131
183
|
const cached = this.#metrics.get(key);
|
|
132
184
|
if (cached !== undefined)
|
|
133
185
|
return cached;
|
|
134
186
|
const context = this.#ensureSurface().context;
|
|
135
|
-
context.font = this.#font(
|
|
187
|
+
context.font = this.#font(face, fontSize);
|
|
136
188
|
const metrics = context.measureText(character);
|
|
137
189
|
const bearingLeft = -metrics.actualBoundingBoxLeft;
|
|
138
190
|
const value = {
|
|
@@ -145,10 +197,10 @@ export class CanvasGlyphRasterizer {
|
|
|
145
197
|
this.#metrics.set(key, value);
|
|
146
198
|
return value;
|
|
147
199
|
}
|
|
148
|
-
rasterize(character,
|
|
200
|
+
rasterize(character, face, rasterSize) {
|
|
149
201
|
// The raster size *is* the face size here: `measureText` and the fill both work in the texels the
|
|
150
202
|
// atlas will store, and the layout's own measurements never pass through this path.
|
|
151
|
-
const glyph = this.glyphMetrics(character,
|
|
203
|
+
const glyph = this.glyphMetrics(character, face, rasterSize);
|
|
152
204
|
if (!(glyph.width > 0) || !(glyph.height > 0))
|
|
153
205
|
return undefined;
|
|
154
206
|
// The ink box is fractional; the mask rounds up so no edge pixel is lost.
|
|
@@ -160,7 +212,7 @@ export class CanvasGlyphRasterizer {
|
|
|
160
212
|
canvas.height = Math.max(canvas.height, height);
|
|
161
213
|
}
|
|
162
214
|
context.clearRect(0, 0, width, height);
|
|
163
|
-
context.font = this.#font(
|
|
215
|
+
context.font = this.#font(face, rasterSize);
|
|
164
216
|
context.textAlign = "left";
|
|
165
217
|
context.textBaseline = "alphabetic";
|
|
166
218
|
context.fillStyle = "#fff";
|
package/dist/text-layout.d.ts
CHANGED
|
@@ -40,12 +40,62 @@ export type GlyphMetrics = {
|
|
|
40
40
|
/** Ink box top edge relative to the baseline, y down: positive is below the baseline. */
|
|
41
41
|
readonly bearingTop: number;
|
|
42
42
|
};
|
|
43
|
+
/**
|
|
44
|
+
* reference `FontStyle`, minus `Oblique(angle)`.
|
|
45
|
+
*
|
|
46
|
+
* Canvas2D accepts the `italic` keyword in its font shorthand and silently drops the slant angle —
|
|
47
|
+
* `oblique 14deg` reads back as `italic` — so an oblique angle could not be honoured and is left out
|
|
48
|
+
* rather than accepted and ignored. A face without an italic face is slanted by the browser.
|
|
49
|
+
*/
|
|
50
|
+
export type TextFontStyle = "normal" | "italic";
|
|
51
|
+
/**
|
|
52
|
+
* reference `FontWidth` stops, spelled as the CSS `font-stretch` keywords Canvas2D understands.
|
|
53
|
+
*
|
|
54
|
+
* reference takes a percentage (`FontWidth::SEMI_CONDENSED` is 87.5), but Canvas2D rejects a percentage in
|
|
55
|
+
* the font shorthand and only accepts these nine keywords. The axis is inert unless the face actually
|
|
56
|
+
* carries a `wdth` axis: a static face like the bundled subset keeps the keyword out of its resolved
|
|
57
|
+
* font, so every stop draws the same width there.
|
|
58
|
+
*/
|
|
59
|
+
export type TextFontWidth = "ultra-condensed" | "extra-condensed" | "condensed" | "semi-condensed" | "normal" | "semi-expanded" | "expanded" | "extra-expanded" | "ultra-expanded";
|
|
60
|
+
/** Every `FontWidth` stop, in reference order. */
|
|
61
|
+
export declare const TEXT_FONT_WIDTHS: readonly TextFontWidth[];
|
|
62
|
+
/** Every `FontStyle` variant this implementation can honour. */
|
|
63
|
+
export declare const TEXT_FONT_STYLES: readonly TextFontStyle[];
|
|
64
|
+
/** reference `FontWeight::NORMAL`, and the value the CSS font shorthand defaults to. */
|
|
65
|
+
export declare const DEFAULT_FONT_WEIGHT = 400;
|
|
66
|
+
/** reference `FontWeight` range, 1 to 1000. */
|
|
67
|
+
export declare const MIN_FONT_WEIGHT = 1;
|
|
68
|
+
export declare const MAX_FONT_WEIGHT = 1000;
|
|
69
|
+
/**
|
|
70
|
+
* One face request: everything that decides which glyphs a character rasterises to.
|
|
71
|
+
*
|
|
72
|
+
* This is the unit the glyph atlas keys on, so two blocks that differ only in weight cannot share a
|
|
73
|
+
* rasterised glyph. A source with a single fixed face reports the parts it cannot change through
|
|
74
|
+
* `GlyphRasterizer.faceKey`.
|
|
75
|
+
*/
|
|
76
|
+
export type FontFace = {
|
|
77
|
+
readonly family: string;
|
|
78
|
+
readonly weight: number;
|
|
79
|
+
readonly style: TextFontStyle;
|
|
80
|
+
readonly width: TextFontWidth;
|
|
81
|
+
};
|
|
82
|
+
/** A face with the axes left optional, which is how callers and spans express one. */
|
|
83
|
+
export type FontFaceOptions = {
|
|
84
|
+
readonly family: string;
|
|
85
|
+
readonly weight?: number | undefined;
|
|
86
|
+
readonly style?: TextFontStyle | undefined;
|
|
87
|
+
readonly width?: TextFontWidth | undefined;
|
|
88
|
+
};
|
|
89
|
+
/** Folds a face's optional axes into the complete request the measurer and the atlas take. */
|
|
90
|
+
export declare function resolveFontFace(options: FontFaceOptions): FontFace;
|
|
91
|
+
/** Every field that can change which glyphs are rasterised, joined into one cache key. */
|
|
92
|
+
export declare function fontFaceKey(face: FontFace): string;
|
|
43
93
|
/** Source of glyph metrics. The browser implementation measures through Canvas2D. */
|
|
44
94
|
export interface TextMeasurer {
|
|
45
|
-
/** Vertical metrics for a `fontSize`-sized
|
|
46
|
-
verticalMetrics(
|
|
95
|
+
/** Vertical metrics for a `fontSize`-sized instance of `face`. */
|
|
96
|
+
verticalMetrics(face: FontFace, fontSize: number): VerticalMetrics;
|
|
47
97
|
/** Metrics for one character. Whitespace returns a zero-size ink box. */
|
|
48
|
-
glyphMetrics(character: string,
|
|
98
|
+
glyphMetrics(character: string, face: FontFace, fontSize: number): GlyphMetrics;
|
|
49
99
|
}
|
|
50
100
|
/** Reference `Justify`. `start`/`end` resolve to left/right in this LTR-only layout. */
|
|
51
101
|
export type TextJustify = "left" | "center" | "right" | "justified" | "start" | "end";
|
|
@@ -62,6 +112,9 @@ export type TextSpanInput = {
|
|
|
62
112
|
readonly color?: Rgba;
|
|
63
113
|
readonly fontSize?: number;
|
|
64
114
|
readonly fontFamily?: string;
|
|
115
|
+
readonly weight?: number;
|
|
116
|
+
readonly style?: TextFontStyle;
|
|
117
|
+
readonly width?: TextFontWidth;
|
|
65
118
|
};
|
|
66
119
|
/** reference `LineHeight` default, `RelativeToFont(1.2)`. */
|
|
67
120
|
export declare const DEFAULT_LINE_HEIGHT_SCALE = 1.2;
|
|
@@ -69,6 +122,12 @@ export type TextLayoutOptions = {
|
|
|
69
122
|
readonly color: Rgba;
|
|
70
123
|
readonly fontSize: number;
|
|
71
124
|
readonly fontFamily: string;
|
|
125
|
+
/** Block default for `FontFace.weight`; spans override it. */
|
|
126
|
+
readonly weight?: number | undefined;
|
|
127
|
+
/** Block default for `FontFace.style`; spans override it. */
|
|
128
|
+
readonly style?: TextFontStyle | undefined;
|
|
129
|
+
/** Block default for `FontFace.width`; spans override it. */
|
|
130
|
+
readonly width?: TextFontWidth | undefined;
|
|
72
131
|
/**
|
|
73
132
|
* Absolute line height in layout units, reference `LineHeight::Px`. When omitted the line height
|
|
74
133
|
* is `lineHeightScale` times the line's own largest font size, which is reference
|
|
@@ -95,7 +154,7 @@ export type TextLayoutOptions = {
|
|
|
95
154
|
/** One laid-out glyph, ready to become an instance once its atlas rectangle is known. */
|
|
96
155
|
export type PositionedGlyph = {
|
|
97
156
|
readonly character: string;
|
|
98
|
-
readonly
|
|
157
|
+
readonly face: FontFace;
|
|
99
158
|
readonly fontSize: number;
|
|
100
159
|
readonly color: Rgba;
|
|
101
160
|
/** Ink box centre relative to the block's anchor point, y down. */
|
package/dist/text-layout.js
CHANGED
|
@@ -18,6 +18,38 @@
|
|
|
18
18
|
* Layout happens in one local space: origin at the block's anchor point, x to the right and
|
|
19
19
|
* **y down**, matching Canvas2D. The renderer flips y for spaces where up is +y.
|
|
20
20
|
*/
|
|
21
|
+
/** Every `FontWidth` stop, in reference order. */
|
|
22
|
+
export const TEXT_FONT_WIDTHS = [
|
|
23
|
+
"ultra-condensed",
|
|
24
|
+
"extra-condensed",
|
|
25
|
+
"condensed",
|
|
26
|
+
"semi-condensed",
|
|
27
|
+
"normal",
|
|
28
|
+
"semi-expanded",
|
|
29
|
+
"expanded",
|
|
30
|
+
"extra-expanded",
|
|
31
|
+
"ultra-expanded",
|
|
32
|
+
];
|
|
33
|
+
/** Every `FontStyle` variant this implementation can honour. */
|
|
34
|
+
export const TEXT_FONT_STYLES = ["normal", "italic"];
|
|
35
|
+
/** reference `FontWeight::NORMAL`, and the value the CSS font shorthand defaults to. */
|
|
36
|
+
export const DEFAULT_FONT_WEIGHT = 400;
|
|
37
|
+
/** reference `FontWeight` range, 1 to 1000. */
|
|
38
|
+
export const MIN_FONT_WEIGHT = 1;
|
|
39
|
+
export const MAX_FONT_WEIGHT = 1000;
|
|
40
|
+
/** Folds a face's optional axes into the complete request the measurer and the atlas take. */
|
|
41
|
+
export function resolveFontFace(options) {
|
|
42
|
+
return {
|
|
43
|
+
family: options.family,
|
|
44
|
+
weight: options.weight ?? DEFAULT_FONT_WEIGHT,
|
|
45
|
+
style: options.style ?? "normal",
|
|
46
|
+
width: options.width ?? "normal",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Every field that can change which glyphs are rasterised, joined into one cache key. */
|
|
50
|
+
export function fontFaceKey(face) {
|
|
51
|
+
return `${face.family}|${face.weight}|${face.width}|${face.style}`;
|
|
52
|
+
}
|
|
21
53
|
const TEXT_ANCHORS = {
|
|
22
54
|
center: [0.5, 0.5],
|
|
23
55
|
"top-left": [0, 0],
|
|
@@ -144,13 +176,20 @@ function measureLine(characters, letterSpacing) {
|
|
|
144
176
|
while (end > 0 && content[end - 1].isSpace)
|
|
145
177
|
end--;
|
|
146
178
|
let fontSize = 0;
|
|
147
|
-
|
|
148
|
-
|
|
179
|
+
let face;
|
|
180
|
+
// The line box follows the line's own largest run, so the face recorded here is that run's.
|
|
181
|
+
for (const character of content) {
|
|
182
|
+
if (character.fontSize > fontSize) {
|
|
183
|
+
fontSize = character.fontSize;
|
|
184
|
+
face = character.face;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
149
187
|
return {
|
|
150
188
|
characters: content.slice(0, end),
|
|
151
189
|
advance: runWidth(content.slice(0, end), letterSpacing),
|
|
152
190
|
fullAdvance: runWidth(content, letterSpacing),
|
|
153
191
|
fontSize,
|
|
192
|
+
face,
|
|
154
193
|
isEmpty: content.length === 0,
|
|
155
194
|
};
|
|
156
195
|
}
|
|
@@ -241,20 +280,31 @@ function countSpaces(characters) {
|
|
|
241
280
|
*/
|
|
242
281
|
export function layoutText(spans, options, measurer) {
|
|
243
282
|
const items = [];
|
|
283
|
+
const blockFace = resolveFontFace({
|
|
284
|
+
family: options.fontFamily,
|
|
285
|
+
weight: options.weight,
|
|
286
|
+
style: options.style,
|
|
287
|
+
width: options.width,
|
|
288
|
+
});
|
|
244
289
|
for (const span of spans) {
|
|
245
290
|
const fontSize = span.fontSize ?? options.fontSize;
|
|
246
|
-
const
|
|
291
|
+
const face = resolveFontFace({
|
|
292
|
+
family: span.fontFamily ?? options.fontFamily,
|
|
293
|
+
weight: span.weight ?? options.weight,
|
|
294
|
+
style: span.style ?? options.style,
|
|
295
|
+
width: span.width ?? options.width,
|
|
296
|
+
});
|
|
247
297
|
const color = span.color ?? options.color;
|
|
248
298
|
// Iterating the string yields code points, so surrogate pairs stay whole.
|
|
249
299
|
for (const character of span.text) {
|
|
250
300
|
if (character === "\n") {
|
|
251
|
-
items.push({ character,
|
|
301
|
+
items.push({ character, face, fontSize, color, advance: 0, width: 0, height: 0, bearingLeft: 0, bearingTop: 0, isSpace: false });
|
|
252
302
|
continue;
|
|
253
303
|
}
|
|
254
|
-
const metrics = measurer.glyphMetrics(character,
|
|
304
|
+
const metrics = measurer.glyphMetrics(character, face, fontSize);
|
|
255
305
|
items.push({
|
|
256
306
|
character,
|
|
257
|
-
|
|
307
|
+
face,
|
|
258
308
|
fontSize,
|
|
259
309
|
color,
|
|
260
310
|
advance: metrics.advance,
|
|
@@ -273,7 +323,7 @@ export function layoutText(spans, options, measurer) {
|
|
|
273
323
|
// instead of being held to the block's default size.
|
|
274
324
|
const boxes = lines.map((line) => {
|
|
275
325
|
const fontSize = line.fontSize > 0 ? line.fontSize : options.fontSize;
|
|
276
|
-
const base = measurer.verticalMetrics(
|
|
326
|
+
const base = measurer.verticalMetrics(line.face ?? blockFace, fontSize);
|
|
277
327
|
const height = options.lineHeight ?? (options.lineHeightScale ?? DEFAULT_LINE_HEIGHT_SCALE) * fontSize;
|
|
278
328
|
return { base, height, halfLeading: (height - (base.ascent + base.descent)) / 2 };
|
|
279
329
|
});
|
|
@@ -310,7 +360,7 @@ export function layoutText(spans, options, measurer) {
|
|
|
310
360
|
if (character.width > 0 && character.height > 0) {
|
|
311
361
|
glyphs.push({
|
|
312
362
|
character: character.character,
|
|
313
|
-
|
|
363
|
+
face: character.face,
|
|
314
364
|
fontSize: character.fontSize,
|
|
315
365
|
color: character.color,
|
|
316
366
|
center: [offsetX + penX + character.bearingLeft + character.width / 2, offsetY + baseline + character.bearingTop + character.height / 2],
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* baking: one set of texels serves every size.
|
|
16
16
|
*/
|
|
17
17
|
import { type GlyphRasterizer, type RasterizedGlyph } from "./text-font.js";
|
|
18
|
-
import type { GlyphMetrics, TextMeasurer, VerticalMetrics } from "./text-layout.js";
|
|
18
|
+
import type { FontFace, GlyphMetrics, TextMeasurer, VerticalMetrics } from "./text-layout.js";
|
|
19
19
|
export declare class BakedMsdfRasterizer implements GlyphRasterizer, TextMeasurer {
|
|
20
20
|
#private;
|
|
21
21
|
readonly encoding: "msdf";
|
|
@@ -30,7 +30,12 @@ export declare class BakedMsdfRasterizer implements GlyphRasterizer, TextMeasure
|
|
|
30
30
|
get ready(): boolean;
|
|
31
31
|
/** Decompresses the inlined page. Idempotent, so calling it per frame is harmless. */
|
|
32
32
|
load(): Promise<void>;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
/**
|
|
34
|
+
* The page stores one face, so the axes cannot change a sliced glyph. Keying on the family alone
|
|
35
|
+
* keeps the atlas from packing the same texels once per requested weight.
|
|
36
|
+
*/
|
|
37
|
+
readonly faceKey: (face: FontFace) => string;
|
|
38
|
+
verticalMetrics(_face: FontFace, fontSize: number): VerticalMetrics;
|
|
39
|
+
glyphMetrics(character: string, _face: FontFace, fontSize: number): GlyphMetrics;
|
|
40
|
+
rasterize(character: string, face: FontFace, fontSize: number): RasterizedGlyph | undefined;
|
|
36
41
|
}
|
package/dist/text-msdf-baked.js
CHANGED
|
@@ -48,11 +48,16 @@ export class BakedMsdfRasterizer {
|
|
|
48
48
|
}
|
|
49
49
|
this.#page = page;
|
|
50
50
|
}
|
|
51
|
-
|
|
51
|
+
/**
|
|
52
|
+
* The page stores one face, so the axes cannot change a sliced glyph. Keying on the family alone
|
|
53
|
+
* keeps the atlas from packing the same texels once per requested weight.
|
|
54
|
+
*/
|
|
55
|
+
faceKey = (face) => face.family;
|
|
56
|
+
verticalMetrics(_face, fontSize) {
|
|
52
57
|
const scale = fontSize / BAKED_MSDF_BAKE_SIZE;
|
|
53
58
|
return { ascent: BAKED_MSDF_ASCENT * scale, descent: BAKED_MSDF_DESCENT * scale };
|
|
54
59
|
}
|
|
55
|
-
glyphMetrics(character,
|
|
60
|
+
glyphMetrics(character, _face, fontSize) {
|
|
56
61
|
const key = `${fontSize}|${character}`;
|
|
57
62
|
const cached = this.#metrics.get(key);
|
|
58
63
|
if (cached !== undefined)
|
|
@@ -71,7 +76,7 @@ export class BakedMsdfRasterizer {
|
|
|
71
76
|
this.#metrics.set(key, value);
|
|
72
77
|
return value;
|
|
73
78
|
}
|
|
74
|
-
rasterize(character,
|
|
79
|
+
rasterize(character, face, fontSize) {
|
|
75
80
|
const page = this.#page;
|
|
76
81
|
if (page === undefined)
|
|
77
82
|
throw new Error("BakedMsdfRasterizer.load() must resolve before glyphs are requested");
|
|
@@ -96,7 +101,7 @@ export class BakedMsdfRasterizer {
|
|
|
96
101
|
}
|
|
97
102
|
// The texels stay at their baked size; only the placement scales with the requested font size.
|
|
98
103
|
const scale = fontSize / BAKED_MSDF_BAKE_SIZE;
|
|
99
|
-
const metrics = this.glyphMetrics(character,
|
|
104
|
+
const metrics = this.glyphMetrics(character, face, fontSize);
|
|
100
105
|
return {
|
|
101
106
|
width,
|
|
102
107
|
height,
|
package/dist/text-msdf-font.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import type { Font } from "opentype.js";
|
|
14
14
|
import { type GlyphRasterizer, type RasterizedGlyph } from "./text-font.js";
|
|
15
15
|
import { type OutlineSegment } from "./text-msdf.js";
|
|
16
|
-
import type { GlyphMetrics, TextMeasurer, VerticalMetrics } from "./text-layout.js";
|
|
16
|
+
import type { FontFace, GlyphMetrics, TextMeasurer, VerticalMetrics } from "./text-layout.js";
|
|
17
17
|
/** The subset of `opentype.js`'s path commands this source understands. */
|
|
18
18
|
type OutlineCommand = {
|
|
19
19
|
readonly type: "M";
|
|
@@ -72,8 +72,14 @@ export declare class OutlineGlyphRasterizer implements GlyphRasterizer, TextMeas
|
|
|
72
72
|
constructor(options?: OutlineGlyphRasterizerOptions);
|
|
73
73
|
/** The face this source reads, for callers that want to name it. */
|
|
74
74
|
get font(): Font;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
/**
|
|
76
|
+
* This source holds one parsed buffer, so a weight, width or slant it was not built with cannot
|
|
77
|
+
* change the outlines it returns. Keying on the family alone keeps the atlas from caching the same
|
|
78
|
+
* glyph once per axis value.
|
|
79
|
+
*/
|
|
80
|
+
readonly faceKey: (face: FontFace) => string;
|
|
81
|
+
verticalMetrics(_face: FontFace, fontSize: number): VerticalMetrics;
|
|
82
|
+
glyphMetrics(character: string, _face: FontFace, fontSize: number): GlyphMetrics;
|
|
83
|
+
rasterize(character: string, _face: FontFace, rasterSize: number): RasterizedGlyph | undefined;
|
|
78
84
|
}
|
|
79
85
|
export {};
|
package/dist/text-msdf-font.js
CHANGED
|
@@ -105,7 +105,13 @@ export class OutlineGlyphRasterizer {
|
|
|
105
105
|
const bottom = Number.isFinite(box.y2) ? box.y2 : top;
|
|
106
106
|
return { left, top, width: Math.max(0, right - left), height: Math.max(0, bottom - top) };
|
|
107
107
|
}
|
|
108
|
-
|
|
108
|
+
/**
|
|
109
|
+
* This source holds one parsed buffer, so a weight, width or slant it was not built with cannot
|
|
110
|
+
* change the outlines it returns. Keying on the family alone keeps the atlas from caching the same
|
|
111
|
+
* glyph once per axis value.
|
|
112
|
+
*/
|
|
113
|
+
faceKey = (face) => face.family;
|
|
114
|
+
verticalMetrics(_face, fontSize) {
|
|
109
115
|
const cached = this.#vertical.get(String(fontSize));
|
|
110
116
|
if (cached !== undefined)
|
|
111
117
|
return cached;
|
|
@@ -114,7 +120,7 @@ export class OutlineGlyphRasterizer {
|
|
|
114
120
|
this.#vertical.set(String(fontSize), value);
|
|
115
121
|
return value;
|
|
116
122
|
}
|
|
117
|
-
glyphMetrics(character,
|
|
123
|
+
glyphMetrics(character, _face, fontSize) {
|
|
118
124
|
const key = `${fontSize}|${character}`;
|
|
119
125
|
const cached = this.#metrics.get(key);
|
|
120
126
|
if (cached !== undefined)
|
|
@@ -131,7 +137,7 @@ export class OutlineGlyphRasterizer {
|
|
|
131
137
|
this.#metrics.set(key, value);
|
|
132
138
|
return value;
|
|
133
139
|
}
|
|
134
|
-
rasterize(character,
|
|
140
|
+
rasterize(character, _face, rasterSize) {
|
|
135
141
|
const ink = this.#inkBox(character, rasterSize);
|
|
136
142
|
if (!(ink.width > 0) || !(ink.height > 0))
|
|
137
143
|
return undefined;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small vector helpers shared by the geometry generators and the example layouts.
|
|
3
|
+
*
|
|
4
|
+
* These live in the library rather than next to the gallery scenes because
|
|
5
|
+
* `geometry-generators.ts` needs them, and a library module must not reach into
|
|
6
|
+
* the example code.
|
|
7
|
+
*/
|
|
8
|
+
import type { Vec3 } from "./frame-draw-list.js";
|
|
9
|
+
export declare function normalizeVector(p: Vec3): Vec3;
|
|
10
|
+
export declare function crossVector(a: Vec3, b: Vec3): Vec3;
|
|
11
|
+
/** Right-handed Rodrigues rotation, equivalent to reference `Vector3.rotated(axis, angle)`. */
|
|
12
|
+
export declare function rotateAroundAxis(p: Vec3, axis: Vec3, angle: number): Vec3;
|
|
13
|
+
export type CircleBasis = {
|
|
14
|
+
readonly x: Vec3;
|
|
15
|
+
readonly y: Vec3;
|
|
16
|
+
readonly z: Vec3;
|
|
17
|
+
};
|
|
18
|
+
/** Orthonormal basis for a circle in the plane whose normal is `normal`. */
|
|
19
|
+
export declare function circleBasis(normal: Vec3, angle: number): CircleBasis;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small vector helpers shared by the geometry generators and the example layouts.
|
|
3
|
+
*
|
|
4
|
+
* These live in the library rather than next to the gallery scenes because
|
|
5
|
+
* `geometry-generators.ts` needs them, and a library module must not reach into
|
|
6
|
+
* the example code.
|
|
7
|
+
*/
|
|
8
|
+
export function normalizeVector(p) {
|
|
9
|
+
const length = Math.hypot(p[0], p[1], p[2]) || 1;
|
|
10
|
+
return [p[0] / length, p[1] / length, p[2] / length];
|
|
11
|
+
}
|
|
12
|
+
export function crossVector(a, b) {
|
|
13
|
+
return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
|
14
|
+
}
|
|
15
|
+
/** Right-handed Rodrigues rotation, equivalent to reference `Vector3.rotated(axis, angle)`. */
|
|
16
|
+
export function rotateAroundAxis(p, axis, angle) {
|
|
17
|
+
const a = normalizeVector(axis);
|
|
18
|
+
const cos = Math.cos(angle);
|
|
19
|
+
const sin = Math.sin(angle);
|
|
20
|
+
const dot = p[0] * a[0] + p[1] * a[1] + p[2] * a[2];
|
|
21
|
+
const cross = crossVector(a, p);
|
|
22
|
+
return [
|
|
23
|
+
p[0] * cos + cross[0] * sin + a[0] * dot * (1 - cos),
|
|
24
|
+
p[1] * cos + cross[1] * sin + a[1] * dot * (1 - cos),
|
|
25
|
+
p[2] * cos + cross[2] * sin + a[2] * dot * (1 - cos),
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
/** Orthonormal basis for a circle in the plane whose normal is `normal`. */
|
|
29
|
+
export function circleBasis(normal, angle) {
|
|
30
|
+
const n = normalizeVector(normal);
|
|
31
|
+
const reference = Math.abs(n[0]) > 0.95 ? [0, 0, 1] : [1, 0, 0];
|
|
32
|
+
const projection = reference[0] * n[0] + reference[1] * n[1] + reference[2] * n[2];
|
|
33
|
+
const planar = [reference[0] - n[0] * projection, reference[1] - n[1] * projection, reference[2] - n[2] * projection];
|
|
34
|
+
const widthAxis = normalizeVector(rotateAroundAxis(normalizeVector(planar), n, angle));
|
|
35
|
+
const heightAxis = normalizeVector(crossVector(n, widthAxis));
|
|
36
|
+
return { x: widthAxis, y: heightAxis, z: n };
|
|
37
|
+
}
|