vgpu-render 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +555 -2
- package/dist/assets/fira-mono-medium-subset.d.ts +4 -0
- package/dist/assets/fira-mono-medium-subset.js +889 -0
- package/dist/assets/fira-mono-msdf-atlas.d.ts +13 -0
- package/dist/assets/fira-mono-msdf-atlas.js +6222 -0
- package/dist/camera-2d.d.ts +43 -0
- package/dist/camera-2d.js +75 -0
- package/dist/color-space.d.ts +32 -0
- package/dist/color-space.js +45 -0
- package/dist/curve.d.ts +16 -0
- package/dist/curve.js +46 -0
- package/dist/debug-draw-3d.d.ts +14 -0
- package/dist/debug-draw-3d.js +36 -0
- package/dist/frame-recorder-2d.d.ts +177 -0
- package/dist/frame-recorder-2d.js +378 -0
- package/dist/frame-recorder.d.ts +361 -0
- package/dist/frame-recorder.js +515 -0
- package/dist/geometry-generators.d.ts +101 -0
- package/dist/geometry-generators.js +481 -0
- package/dist/gpu-debug-draw-2d.d.ts +48 -0
- package/dist/gpu-debug-draw-2d.js +450 -0
- package/dist/gpu-debug-draw-3d.d.ts +52 -0
- package/dist/gpu-debug-draw-3d.js +890 -0
- package/dist/gpu-instancing.d.ts +34 -0
- package/dist/gpu-instancing.js +93 -0
- package/dist/gpu-text-3d.d.ts +98 -0
- package/dist/gpu-text-3d.js +467 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +21 -0
- package/dist/infinite-grid-2d.d.ts +73 -0
- package/dist/infinite-grid-2d.js +253 -0
- package/dist/infinite-grid-3d.d.ts +134 -0
- package/dist/infinite-grid-3d.js +407 -0
- package/dist/layout-3d.d.ts +227 -0
- package/dist/layout-3d.js +226 -0
- package/dist/mesh-batch-3d.d.ts +10 -0
- package/dist/mesh-batch-3d.js +67 -0
- package/dist/scene-examples.d.ts +39 -0
- package/dist/scene-examples.js +200 -0
- package/dist/scenes-2d.d.ts +18 -0
- package/dist/scenes-2d.js +160 -0
- package/dist/scenes-3d.d.ts +46 -0
- package/dist/scenes-3d.js +1994 -0
- package/dist/shape2d-shader.d.ts +14 -0
- package/dist/shape2d-shader.js +287 -0
- package/dist/text-atlas.d.ts +174 -0
- package/dist/text-atlas.js +495 -0
- package/dist/text-font.d.ts +149 -0
- package/dist/text-font.js +209 -0
- package/dist/text-layout.d.ts +116 -0
- package/dist/text-layout.js +340 -0
- package/dist/text-msdf-baked.d.ts +36 -0
- package/dist/text-msdf-baked.js +112 -0
- package/dist/text-msdf-font.d.ts +79 -0
- package/dist/text-msdf-font.js +153 -0
- package/dist/text-msdf.d.ts +70 -0
- package/dist/text-msdf.js +242 -0
- package/dist/text-raster.d.ts +40 -0
- package/dist/text-raster.js +60 -0
- package/dist/text-scenes.d.ts +85 -0
- package/dist/text-scenes.js +338 -0
- package/dist/text-sdf.d.ts +28 -0
- package/dist/text-sdf.js +119 -0
- package/package.json +80 -4
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prebaked multi-channel distance field glyph source.
|
|
3
|
+
*
|
|
4
|
+
* The counterpart to `OutlineGlyphRasterizer`: instead of parsing a face and running the encoder on
|
|
5
|
+
* the first frame a glyph appears, this reads the atlas that `bun scripts/bake-msdf-atlas.ts`
|
|
6
|
+
* produced and slices each glyph out of it. Nothing here links `opentype.js`, and no encoding ever
|
|
7
|
+
* runs — at the cost of a fixed character set and a payload that ships whether or not it is used.
|
|
8
|
+
*
|
|
9
|
+
* The page travels gzipped, so `load()` has to resolve before any glyph is asked for. Decompression
|
|
10
|
+
* uses the platform `DecompressionStream`, which is why the payload is not a PNG: it keeps the whole
|
|
11
|
+
* path free of image decoding and usable in Node, where the tests run.
|
|
12
|
+
*
|
|
13
|
+
* Because the page stores every glyph at one fixed raster, the metrics it carries are scaled by
|
|
14
|
+
* `fontSize / bakeSize` rather than re-rasterised. That is exactly what makes a distance field worth
|
|
15
|
+
* baking: one set of texels serves every size.
|
|
16
|
+
*/
|
|
17
|
+
import { type GlyphRasterizer, type RasterizedGlyph } from "./text-font.js";
|
|
18
|
+
import type { GlyphMetrics, TextMeasurer, VerticalMetrics } from "./text-layout.js";
|
|
19
|
+
export declare class BakedMsdfRasterizer implements GlyphRasterizer, TextMeasurer {
|
|
20
|
+
#private;
|
|
21
|
+
readonly encoding: "msdf";
|
|
22
|
+
readonly distanceRange = 4;
|
|
23
|
+
readonly bakeSize = 24;
|
|
24
|
+
/**
|
|
25
|
+
* Every glyph already sits in a page baked at `bakeSize`, so another raster size cannot be
|
|
26
|
+
* honoured: the renderer keeps this source on its own raster instead of following the screen.
|
|
27
|
+
*/
|
|
28
|
+
readonly dynamicRaster = false;
|
|
29
|
+
/** True once the page is decompressed and glyphs can be sliced out of it. */
|
|
30
|
+
get ready(): boolean;
|
|
31
|
+
/** Decompresses the inlined page. Idempotent, so calling it per frame is harmless. */
|
|
32
|
+
load(): Promise<void>;
|
|
33
|
+
verticalMetrics(_fontFamily: string, fontSize: number): VerticalMetrics;
|
|
34
|
+
glyphMetrics(character: string, _fontFamily: string, fontSize: number): GlyphMetrics;
|
|
35
|
+
rasterize(character: string, fontFamily: string, fontSize: number): RasterizedGlyph | undefined;
|
|
36
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prebaked multi-channel distance field glyph source.
|
|
3
|
+
*
|
|
4
|
+
* The counterpart to `OutlineGlyphRasterizer`: instead of parsing a face and running the encoder on
|
|
5
|
+
* the first frame a glyph appears, this reads the atlas that `bun scripts/bake-msdf-atlas.ts`
|
|
6
|
+
* produced and slices each glyph out of it. Nothing here links `opentype.js`, and no encoding ever
|
|
7
|
+
* runs — at the cost of a fixed character set and a payload that ships whether or not it is used.
|
|
8
|
+
*
|
|
9
|
+
* The page travels gzipped, so `load()` has to resolve before any glyph is asked for. Decompression
|
|
10
|
+
* uses the platform `DecompressionStream`, which is why the payload is not a PNG: it keeps the whole
|
|
11
|
+
* path free of image decoding and usable in Node, where the tests run.
|
|
12
|
+
*
|
|
13
|
+
* Because the page stores every glyph at one fixed raster, the metrics it carries are scaled by
|
|
14
|
+
* `fontSize / bakeSize` rather than re-rasterised. That is exactly what makes a distance field worth
|
|
15
|
+
* baking: one set of texels serves every size.
|
|
16
|
+
*/
|
|
17
|
+
import { BAKED_MSDF_ASCENT, BAKED_MSDF_BAKE_SIZE, BAKED_MSDF_DESCENT, BAKED_MSDF_DISTANCE_RANGE, BAKED_MSDF_GLYPHS, BAKED_MSDF_PAGE_GZIP_BASE64, BAKED_MSDF_PAGE_SIZE, } from "./assets/fira-mono-msdf-atlas.js";
|
|
18
|
+
import { decodeBase64Bytes } from "./text-font.js";
|
|
19
|
+
function bakedGlyph(character) {
|
|
20
|
+
return BAKED_MSDF_GLYPHS[character];
|
|
21
|
+
}
|
|
22
|
+
export class BakedMsdfRasterizer {
|
|
23
|
+
encoding = "msdf";
|
|
24
|
+
distanceRange = BAKED_MSDF_DISTANCE_RANGE;
|
|
25
|
+
bakeSize = BAKED_MSDF_BAKE_SIZE;
|
|
26
|
+
/**
|
|
27
|
+
* Every glyph already sits in a page baked at `bakeSize`, so another raster size cannot be
|
|
28
|
+
* honoured: the renderer keeps this source on its own raster instead of following the screen.
|
|
29
|
+
*/
|
|
30
|
+
dynamicRaster = false;
|
|
31
|
+
#metrics = new Map();
|
|
32
|
+
#page;
|
|
33
|
+
/** True once the page is decompressed and glyphs can be sliced out of it. */
|
|
34
|
+
get ready() {
|
|
35
|
+
return this.#page !== undefined;
|
|
36
|
+
}
|
|
37
|
+
/** Decompresses the inlined page. Idempotent, so calling it per frame is harmless. */
|
|
38
|
+
async load() {
|
|
39
|
+
if (this.#page !== undefined)
|
|
40
|
+
return;
|
|
41
|
+
const compressed = decodeBase64Bytes(BAKED_MSDF_PAGE_GZIP_BASE64);
|
|
42
|
+
const stream = new Blob([compressed]).stream().pipeThrough(new DecompressionStream("gzip"));
|
|
43
|
+
const buffer = await new Response(stream).arrayBuffer();
|
|
44
|
+
const page = new Uint8Array(buffer);
|
|
45
|
+
const expected = BAKED_MSDF_PAGE_SIZE * BAKED_MSDF_PAGE_SIZE * 4;
|
|
46
|
+
if (page.byteLength !== expected) {
|
|
47
|
+
throw new Error(`the prebaked atlas decompressed to ${page.byteLength} bytes, expected ${expected}`);
|
|
48
|
+
}
|
|
49
|
+
this.#page = page;
|
|
50
|
+
}
|
|
51
|
+
verticalMetrics(_fontFamily, fontSize) {
|
|
52
|
+
const scale = fontSize / BAKED_MSDF_BAKE_SIZE;
|
|
53
|
+
return { ascent: BAKED_MSDF_ASCENT * scale, descent: BAKED_MSDF_DESCENT * scale };
|
|
54
|
+
}
|
|
55
|
+
glyphMetrics(character, _fontFamily, fontSize) {
|
|
56
|
+
const key = `${fontSize}|${character}`;
|
|
57
|
+
const cached = this.#metrics.get(key);
|
|
58
|
+
if (cached !== undefined)
|
|
59
|
+
return cached;
|
|
60
|
+
const baked = bakedGlyph(character);
|
|
61
|
+
const scale = fontSize / BAKED_MSDF_BAKE_SIZE;
|
|
62
|
+
const value = baked === undefined
|
|
63
|
+
? { advance: 0, width: 0, height: 0, bearingLeft: 0, bearingTop: 0 }
|
|
64
|
+
: {
|
|
65
|
+
advance: baked[8] * scale,
|
|
66
|
+
width: baked[6] * scale,
|
|
67
|
+
height: baked[7] * scale,
|
|
68
|
+
bearingLeft: baked[4] * scale,
|
|
69
|
+
bearingTop: baked[5] * scale,
|
|
70
|
+
};
|
|
71
|
+
this.#metrics.set(key, value);
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
rasterize(character, fontFamily, fontSize) {
|
|
75
|
+
const page = this.#page;
|
|
76
|
+
if (page === undefined)
|
|
77
|
+
throw new Error("BakedMsdfRasterizer.load() must resolve before glyphs are requested");
|
|
78
|
+
const baked = bakedGlyph(character);
|
|
79
|
+
if (baked === undefined)
|
|
80
|
+
return undefined;
|
|
81
|
+
const pageSize = BAKED_MSDF_PAGE_SIZE;
|
|
82
|
+
const left = Math.round(baked[0] * pageSize);
|
|
83
|
+
const top = Math.round(baked[1] * pageSize);
|
|
84
|
+
// `ceil`, not `round`: the atlas maps the ink box exactly (`inkWidth` below), so the texels it
|
|
85
|
+
// stores have to be at least as large as that box. Rounding down lets the UV rectangle read past
|
|
86
|
+
// the columns this slice wrote, which pulls the glyph's edge inwards.
|
|
87
|
+
const width = Math.ceil((baked[2] - baked[0]) * pageSize);
|
|
88
|
+
const height = Math.ceil((baked[3] - baked[1]) * pageSize);
|
|
89
|
+
// Whitespace is in the table for its advance, but there is no ink box to slice out of the page.
|
|
90
|
+
if (width <= 0 || height <= 0)
|
|
91
|
+
return undefined;
|
|
92
|
+
const texels = new Uint8Array(width * height * 4);
|
|
93
|
+
for (let row = 0; row < height; row++) {
|
|
94
|
+
const from = ((top + row) * pageSize + left) * 4;
|
|
95
|
+
texels.set(page.subarray(from, from + width * 4), row * width * 4);
|
|
96
|
+
}
|
|
97
|
+
// The texels stay at their baked size; only the placement scales with the requested font size.
|
|
98
|
+
const scale = fontSize / BAKED_MSDF_BAKE_SIZE;
|
|
99
|
+
const metrics = this.glyphMetrics(character, fontFamily, fontSize);
|
|
100
|
+
return {
|
|
101
|
+
width,
|
|
102
|
+
height,
|
|
103
|
+
texels,
|
|
104
|
+
// The bake carries the ink box at the bake size, which is what the atlas has to map.
|
|
105
|
+
inkWidth: baked[6],
|
|
106
|
+
inkHeight: baked[7],
|
|
107
|
+
bearingLeft: metrics.bearingLeft,
|
|
108
|
+
bearingTop: metrics.bearingTop,
|
|
109
|
+
advance: metrics.advance,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outline-backed glyph source, using `opentype.js` to read the bundled TrueType face.
|
|
3
|
+
*
|
|
4
|
+
* Canvas2D can only hand back a coverage mask, and a mask has lost the edges that a multi-channel
|
|
5
|
+
* distance field is built from. Outlines are where those edges still exist, so this source parses
|
|
6
|
+
* the inlined subset and encodes each glyph with `msdfFromOutline`.
|
|
7
|
+
*
|
|
8
|
+
* It is the on-demand counterpart to a prebaked atlas: it works for any character the face covers
|
|
9
|
+
* and any face at all, at the cost of running the encoder on the first frame each glyph appears.
|
|
10
|
+
* Coordinates follow the Canvas2D convention the rest of the text stack uses — x to the right, y
|
|
11
|
+
* down, the pen at (0, 0) and the baseline at y = 0 — which is also what `glyph.getPath` produces.
|
|
12
|
+
*/
|
|
13
|
+
import type { Font } from "opentype.js";
|
|
14
|
+
import { type GlyphRasterizer, type RasterizedGlyph } from "./text-font.js";
|
|
15
|
+
import { type OutlineSegment } from "./text-msdf.js";
|
|
16
|
+
import type { GlyphMetrics, TextMeasurer, VerticalMetrics } from "./text-layout.js";
|
|
17
|
+
/** The subset of `opentype.js`'s path commands this source understands. */
|
|
18
|
+
type OutlineCommand = {
|
|
19
|
+
readonly type: "M";
|
|
20
|
+
readonly x: number;
|
|
21
|
+
readonly y: number;
|
|
22
|
+
} | {
|
|
23
|
+
readonly type: "L";
|
|
24
|
+
readonly x: number;
|
|
25
|
+
readonly y: number;
|
|
26
|
+
} | {
|
|
27
|
+
readonly type: "Q";
|
|
28
|
+
readonly x1: number;
|
|
29
|
+
readonly y1: number;
|
|
30
|
+
readonly x: number;
|
|
31
|
+
readonly y: number;
|
|
32
|
+
} | {
|
|
33
|
+
readonly type: "C";
|
|
34
|
+
readonly x1: number;
|
|
35
|
+
readonly y1: number;
|
|
36
|
+
readonly x2: number;
|
|
37
|
+
readonly y2: number;
|
|
38
|
+
readonly x: number;
|
|
39
|
+
readonly y: number;
|
|
40
|
+
} | {
|
|
41
|
+
readonly type: "Z";
|
|
42
|
+
};
|
|
43
|
+
export type OutlineGlyphRasterizerOptions = {
|
|
44
|
+
/** TrueType bytes to read. Defaults to the inlined subset. */
|
|
45
|
+
readonly font?: ArrayBuffer;
|
|
46
|
+
/** Distance in texels either side of the edge that the encoded field covers. */
|
|
47
|
+
readonly distanceRange?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Default rasterisation scale: a request that names no scale of its own is encoded at
|
|
50
|
+
* `fontSize * rasterScale` texels while layout and quads stay in font pixels, exactly as the
|
|
51
|
+
* Canvas2D sources do it. The renderer overrides it per block with the size the block covers on
|
|
52
|
+
* screen (see `text-raster`), so the atlas asks for the finer size and the same outline is encoded
|
|
53
|
+
* at a raster that matches it.
|
|
54
|
+
*/
|
|
55
|
+
readonly rasterScale?: number;
|
|
56
|
+
/**
|
|
57
|
+
* Whether the renderer drives this source's raster size from the size a block covers on screen.
|
|
58
|
+
* Outlines encode at any size, so it defaults to `true`; a fixed raster is only for a comparison
|
|
59
|
+
* that has to stay at one raster size on purpose.
|
|
60
|
+
*/
|
|
61
|
+
readonly dynamicRaster?: boolean;
|
|
62
|
+
};
|
|
63
|
+
/** Turns a path's command list into outline segments, offset so the ink box starts at the origin. */
|
|
64
|
+
export declare function commandsToSegments(commands: readonly OutlineCommand[], offsetX: number, offsetY: number): readonly OutlineSegment[];
|
|
65
|
+
export declare class OutlineGlyphRasterizer implements GlyphRasterizer, TextMeasurer {
|
|
66
|
+
#private;
|
|
67
|
+
readonly encoding: "msdf";
|
|
68
|
+
readonly distanceRange: number;
|
|
69
|
+
readonly rasterScale: number;
|
|
70
|
+
/** Whether the renderer drives this source's raster per block. See `dynamicRaster` in the options. */
|
|
71
|
+
readonly dynamicRaster: boolean;
|
|
72
|
+
constructor(options?: OutlineGlyphRasterizerOptions);
|
|
73
|
+
/** The face this source reads, for callers that want to name it. */
|
|
74
|
+
get font(): Font;
|
|
75
|
+
verticalMetrics(_fontFamily: string, fontSize: number): VerticalMetrics;
|
|
76
|
+
glyphMetrics(character: string, _fontFamily: string, fontSize: number): GlyphMetrics;
|
|
77
|
+
rasterize(character: string, _fontFamily: string, rasterSize: number): RasterizedGlyph | undefined;
|
|
78
|
+
}
|
|
79
|
+
export {};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outline-backed glyph source, using `opentype.js` to read the bundled TrueType face.
|
|
3
|
+
*
|
|
4
|
+
* Canvas2D can only hand back a coverage mask, and a mask has lost the edges that a multi-channel
|
|
5
|
+
* distance field is built from. Outlines are where those edges still exist, so this source parses
|
|
6
|
+
* the inlined subset and encodes each glyph with `msdfFromOutline`.
|
|
7
|
+
*
|
|
8
|
+
* It is the on-demand counterpart to a prebaked atlas: it works for any character the face covers
|
|
9
|
+
* and any face at all, at the cost of running the encoder on the first frame each glyph appears.
|
|
10
|
+
* Coordinates follow the Canvas2D convention the rest of the text stack uses — x to the right, y
|
|
11
|
+
* down, the pen at (0, 0) and the baseline at y = 0 — which is also what `glyph.getPath` produces.
|
|
12
|
+
*/
|
|
13
|
+
import * as opentype from "opentype.js";
|
|
14
|
+
import { decodeEmbeddedTrueTypeFont } from "./text-font.js";
|
|
15
|
+
import { msdfFromOutline } from "./text-msdf.js";
|
|
16
|
+
import { DEFAULT_SDF_RANGE } from "./text-sdf.js";
|
|
17
|
+
/** Turns a path's command list into outline segments, offset so the ink box starts at the origin. */
|
|
18
|
+
export function commandsToSegments(commands, offsetX, offsetY) {
|
|
19
|
+
const segments = [];
|
|
20
|
+
let current;
|
|
21
|
+
let subpathStart;
|
|
22
|
+
for (const command of commands) {
|
|
23
|
+
const to = command.type === "Z" ? undefined : [command.x - offsetX, command.y - offsetY];
|
|
24
|
+
switch (command.type) {
|
|
25
|
+
case "M":
|
|
26
|
+
current = to;
|
|
27
|
+
subpathStart = to;
|
|
28
|
+
break;
|
|
29
|
+
case "L":
|
|
30
|
+
if (current !== undefined && to !== undefined)
|
|
31
|
+
segments.push({ from: current, to });
|
|
32
|
+
current = to;
|
|
33
|
+
break;
|
|
34
|
+
case "Q":
|
|
35
|
+
if (current !== undefined && to !== undefined) {
|
|
36
|
+
segments.push({ from: current, control1: [command.x1 - offsetX, command.y1 - offsetY], to });
|
|
37
|
+
}
|
|
38
|
+
current = to;
|
|
39
|
+
break;
|
|
40
|
+
case "C":
|
|
41
|
+
if (current !== undefined && to !== undefined) {
|
|
42
|
+
segments.push({
|
|
43
|
+
from: current,
|
|
44
|
+
control1: [command.x1 - offsetX, command.y1 - offsetY],
|
|
45
|
+
control2: [command.x2 - offsetX, command.y2 - offsetY],
|
|
46
|
+
to,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
current = to;
|
|
50
|
+
break;
|
|
51
|
+
case "Z":
|
|
52
|
+
// An explicit closing edge only if the contour did not already return to its start.
|
|
53
|
+
if (current !== undefined && subpathStart !== undefined) {
|
|
54
|
+
const [cx, cy] = current;
|
|
55
|
+
const [sx, sy] = subpathStart;
|
|
56
|
+
if (cx !== sx || cy !== sy)
|
|
57
|
+
segments.push({ from: current, to: subpathStart });
|
|
58
|
+
current = subpathStart;
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return segments;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolves `parse` from either build of `opentype.js`.
|
|
67
|
+
*
|
|
68
|
+
* The package ships a CommonJS entry and an ESM entry without an `exports` map, so Node's ESM
|
|
69
|
+
* loader only exposes the CommonJS default export while bundlers pick the ESM module and its named
|
|
70
|
+
* exports. Reading whichever shape we were handed keeps both paths working.
|
|
71
|
+
*/
|
|
72
|
+
const parseFont = (opentype.parse ??
|
|
73
|
+
opentype.default.parse);
|
|
74
|
+
export class OutlineGlyphRasterizer {
|
|
75
|
+
encoding = "msdf";
|
|
76
|
+
distanceRange;
|
|
77
|
+
rasterScale;
|
|
78
|
+
/** Whether the renderer drives this source's raster per block. See `dynamicRaster` in the options. */
|
|
79
|
+
dynamicRaster;
|
|
80
|
+
#font;
|
|
81
|
+
#metrics = new Map();
|
|
82
|
+
#vertical = new Map();
|
|
83
|
+
constructor(options = {}) {
|
|
84
|
+
this.#font = parseFont(options.font ?? decodeEmbeddedTrueTypeFont().buffer);
|
|
85
|
+
this.distanceRange = options.distanceRange ?? DEFAULT_SDF_RANGE;
|
|
86
|
+
this.rasterScale = options.rasterScale ?? 1;
|
|
87
|
+
this.dynamicRaster = options.dynamicRaster ?? true;
|
|
88
|
+
}
|
|
89
|
+
/** The face this source reads, for callers that want to name it. */
|
|
90
|
+
get font() {
|
|
91
|
+
return this.#font;
|
|
92
|
+
}
|
|
93
|
+
#scale(fontSize) {
|
|
94
|
+
return fontSize / this.#font.unitsPerEm;
|
|
95
|
+
}
|
|
96
|
+
/** Ink box of one character at `fontSize`, in the pen's own coordinates. */
|
|
97
|
+
#inkBox(character, fontSize) {
|
|
98
|
+
const path = this.#font.charToGlyph(character).getPath(0, 0, fontSize);
|
|
99
|
+
if (path.commands.length === 0)
|
|
100
|
+
return { left: 0, top: 0, width: 0, height: 0 };
|
|
101
|
+
const box = path.getBoundingBox();
|
|
102
|
+
const left = Number.isFinite(box.x1) ? box.x1 : 0;
|
|
103
|
+
const top = Number.isFinite(box.y1) ? box.y1 : 0;
|
|
104
|
+
const right = Number.isFinite(box.x2) ? box.x2 : left;
|
|
105
|
+
const bottom = Number.isFinite(box.y2) ? box.y2 : top;
|
|
106
|
+
return { left, top, width: Math.max(0, right - left), height: Math.max(0, bottom - top) };
|
|
107
|
+
}
|
|
108
|
+
verticalMetrics(_fontFamily, fontSize) {
|
|
109
|
+
const cached = this.#vertical.get(String(fontSize));
|
|
110
|
+
if (cached !== undefined)
|
|
111
|
+
return cached;
|
|
112
|
+
const scale = this.#scale(fontSize);
|
|
113
|
+
const value = { ascent: this.#font.ascender * scale, descent: -this.#font.descender * scale };
|
|
114
|
+
this.#vertical.set(String(fontSize), value);
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
glyphMetrics(character, _fontFamily, fontSize) {
|
|
118
|
+
const key = `${fontSize}|${character}`;
|
|
119
|
+
const cached = this.#metrics.get(key);
|
|
120
|
+
if (cached !== undefined)
|
|
121
|
+
return cached;
|
|
122
|
+
const ink = this.#inkBox(character, fontSize);
|
|
123
|
+
const advance = (this.#font.charToGlyph(character).advanceWidth ?? 0) * this.#scale(fontSize);
|
|
124
|
+
const value = {
|
|
125
|
+
advance,
|
|
126
|
+
width: ink.width,
|
|
127
|
+
height: ink.height,
|
|
128
|
+
bearingLeft: ink.left,
|
|
129
|
+
bearingTop: ink.top,
|
|
130
|
+
};
|
|
131
|
+
this.#metrics.set(key, value);
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
rasterize(character, _fontFamily, rasterSize) {
|
|
135
|
+
const ink = this.#inkBox(character, rasterSize);
|
|
136
|
+
if (!(ink.width > 0) || !(ink.height > 0))
|
|
137
|
+
return undefined;
|
|
138
|
+
const width = Math.max(1, Math.ceil(ink.width));
|
|
139
|
+
const height = Math.max(1, Math.ceil(ink.height));
|
|
140
|
+
const path = this.#font.charToGlyph(character).getPath(0, 0, rasterSize);
|
|
141
|
+
const segments = commandsToSegments(path.commands, ink.left, ink.top);
|
|
142
|
+
return {
|
|
143
|
+
width,
|
|
144
|
+
height,
|
|
145
|
+
texels: msdfFromOutline(segments, width, height, this.distanceRange),
|
|
146
|
+
inkWidth: ink.width,
|
|
147
|
+
inkHeight: ink.height,
|
|
148
|
+
bearingLeft: ink.left,
|
|
149
|
+
bearingTop: ink.top,
|
|
150
|
+
advance: (this.#font.charToGlyph(character).advanceWidth ?? 0) * this.#scale(rasterSize),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-channel signed distance field generation from glyph outlines.
|
|
3
|
+
*
|
|
4
|
+
* A single distance channel stores one number per texel: how far the nearest edge is. That is
|
|
5
|
+
* enough everywhere except near a corner, where two edges meet and the single value can only
|
|
6
|
+
* describe one of them, so the corner rounds off when it is rebuilt. MSDF stores a distance per
|
|
7
|
+
* *edge group* instead — three of them, in RGB — and the shader takes the median. At a corner the
|
|
8
|
+
* three channels disagree and the median keeps the one that is still nearly correct, which is what
|
|
9
|
+
* preserves the point.
|
|
10
|
+
*
|
|
11
|
+
* Curves are chopped into short straight pieces before distances are measured: exact distances to a
|
|
12
|
+
* bezier would need a numerical solve per texel, while a chord error under half a texel is invisible
|
|
13
|
+
* at the sizes this is used at. Edge group assignment follows msdfgen's rule of thumb — adjacent
|
|
14
|
+
* edges that run nearly straight through their shared vertex keep the same group, everything else
|
|
15
|
+
* advances around a three-colour wheel.
|
|
16
|
+
*
|
|
17
|
+
* The median only equals the distance to the *nearest* edge where at least two channels are
|
|
18
|
+
* saturated. Inside a stroke that sees two or three edges it resolves to the middle distance
|
|
19
|
+
* instead, and the rebuilt edge drifts by up to half a texel: measured against a super-sampled
|
|
20
|
+
* coverage reference at 24 px, the raw field is off by 0.073 (mean) and 0.513 (worst texel) in alpha
|
|
21
|
+
* units. msdfgen handles that with an error-correction pass after generating the field, whose
|
|
22
|
+
* flagship form equalises every flagged texel to its own median. That was measured here too and it
|
|
23
|
+
* made the reconstruction *worse* (0.078), because this encoder gives every channel the texel's own
|
|
24
|
+
* inside/outside sign rather than a per-edge signed distance, so the channels differ only in which
|
|
25
|
+
* edge is nearest and equalising them flattens the gradient the filter relies on.
|
|
26
|
+
*
|
|
27
|
+
* What does help is correcting the median itself against the exact shape distance: only the channel
|
|
28
|
+
* or channels needed to make that distance the middle value move, so the other channel keeps its
|
|
29
|
+
* gradient. The same measurement drops to 0.043, against 0.040 for a field whose whole edge band is
|
|
30
|
+
* made exactly right — the remainder is the resolution limit of one distance per texel plus linear
|
|
31
|
+
* filtering, not something another pass can recover.
|
|
32
|
+
*
|
|
33
|
+
* Alpha is not part of the median: it keeps the same exact shape distance uncorrected, which is what
|
|
34
|
+
* an outline of uniform width needs and what the median cannot provide at a corner.
|
|
35
|
+
*/
|
|
36
|
+
export type Point = readonly [number, number];
|
|
37
|
+
/** One outline segment: a straight line, or a curve carrying its control points. */
|
|
38
|
+
export type OutlineSegment = {
|
|
39
|
+
readonly from: Point;
|
|
40
|
+
readonly control1?: Point;
|
|
41
|
+
readonly control2?: Point;
|
|
42
|
+
readonly to: Point;
|
|
43
|
+
};
|
|
44
|
+
type ColoredLine = {
|
|
45
|
+
readonly from: Point;
|
|
46
|
+
readonly to: Point;
|
|
47
|
+
readonly channel: 0 | 1 | 2;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Assigns each segment to one of three channels. Segments that continue the same edge share a
|
|
51
|
+
* channel; a join that actually turns the outline advances to the next one, so the three edges of a
|
|
52
|
+
* corner end up on three different channels.
|
|
53
|
+
*/
|
|
54
|
+
export declare function colorOutlineEdges(segments: readonly OutlineSegment[]): Uint8Array;
|
|
55
|
+
/** Chops curves into straight pieces, each inheriting its parent's channel. */
|
|
56
|
+
export declare function flattenOutline(segments: readonly OutlineSegment[], channels: Uint8Array, steps?: number): readonly ColoredLine[];
|
|
57
|
+
/**
|
|
58
|
+
* Renders one glyph outline into an RGBA8 distance field.
|
|
59
|
+
*
|
|
60
|
+
* Each channel holds the signed distance to the nearest edge in its group, averaged over the texel
|
|
61
|
+
* centre, and encoded as `distance / (2 × range) + 0.5` so 128 is the edge. Channels no edge was
|
|
62
|
+
* assigned to saturate, because a median over two real distances and one saturated value still
|
|
63
|
+
* resolves to a real edge. Alpha carries the *true* distance to the shape, which is msdfgen's
|
|
64
|
+
* `mtsdf` layout: the median in RGB preserves the corners a single channel rounds off, while a
|
|
65
|
+
* uniform-width outline reads alpha, because a per-edge median dents exactly at a corner. The field
|
|
66
|
+
* is then corrected against the exact shape distance — see the error-correction note at the top of
|
|
67
|
+
* this file.
|
|
68
|
+
*/
|
|
69
|
+
export declare function msdfFromOutline(segments: readonly OutlineSegment[], width: number, height: number, range: number, steps?: number): Uint8Array;
|
|
70
|
+
export {};
|