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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +555 -2
  3. package/dist/assets/fira-mono-medium-subset.d.ts +4 -0
  4. package/dist/assets/fira-mono-medium-subset.js +889 -0
  5. package/dist/assets/fira-mono-msdf-atlas.d.ts +13 -0
  6. package/dist/assets/fira-mono-msdf-atlas.js +6222 -0
  7. package/dist/camera-2d.d.ts +43 -0
  8. package/dist/camera-2d.js +75 -0
  9. package/dist/color-space.d.ts +32 -0
  10. package/dist/color-space.js +45 -0
  11. package/dist/curve.d.ts +16 -0
  12. package/dist/curve.js +46 -0
  13. package/dist/debug-draw-3d.d.ts +14 -0
  14. package/dist/debug-draw-3d.js +36 -0
  15. package/dist/frame-recorder-2d.d.ts +177 -0
  16. package/dist/frame-recorder-2d.js +378 -0
  17. package/dist/frame-recorder.d.ts +361 -0
  18. package/dist/frame-recorder.js +515 -0
  19. package/dist/geometry-generators.d.ts +101 -0
  20. package/dist/geometry-generators.js +481 -0
  21. package/dist/gpu-debug-draw-2d.d.ts +48 -0
  22. package/dist/gpu-debug-draw-2d.js +450 -0
  23. package/dist/gpu-debug-draw-3d.d.ts +52 -0
  24. package/dist/gpu-debug-draw-3d.js +890 -0
  25. package/dist/gpu-instancing.d.ts +34 -0
  26. package/dist/gpu-instancing.js +93 -0
  27. package/dist/gpu-text-3d.d.ts +98 -0
  28. package/dist/gpu-text-3d.js +467 -0
  29. package/dist/index.d.ts +21 -0
  30. package/dist/index.js +21 -0
  31. package/dist/infinite-grid-2d.d.ts +73 -0
  32. package/dist/infinite-grid-2d.js +253 -0
  33. package/dist/infinite-grid-3d.d.ts +134 -0
  34. package/dist/infinite-grid-3d.js +407 -0
  35. package/dist/layout-3d.d.ts +227 -0
  36. package/dist/layout-3d.js +226 -0
  37. package/dist/mesh-batch-3d.d.ts +10 -0
  38. package/dist/mesh-batch-3d.js +67 -0
  39. package/dist/scene-examples.d.ts +39 -0
  40. package/dist/scene-examples.js +200 -0
  41. package/dist/scenes-2d.d.ts +18 -0
  42. package/dist/scenes-2d.js +160 -0
  43. package/dist/scenes-3d.d.ts +46 -0
  44. package/dist/scenes-3d.js +1994 -0
  45. package/dist/shape2d-shader.d.ts +14 -0
  46. package/dist/shape2d-shader.js +287 -0
  47. package/dist/text-atlas.d.ts +174 -0
  48. package/dist/text-atlas.js +495 -0
  49. package/dist/text-font.d.ts +149 -0
  50. package/dist/text-font.js +209 -0
  51. package/dist/text-layout.d.ts +116 -0
  52. package/dist/text-layout.js +340 -0
  53. package/dist/text-msdf-baked.d.ts +36 -0
  54. package/dist/text-msdf-baked.js +112 -0
  55. package/dist/text-msdf-font.d.ts +79 -0
  56. package/dist/text-msdf-font.js +153 -0
  57. package/dist/text-msdf.d.ts +70 -0
  58. package/dist/text-msdf.js +242 -0
  59. package/dist/text-raster.d.ts +40 -0
  60. package/dist/text-raster.js +60 -0
  61. package/dist/text-scenes.d.ts +85 -0
  62. package/dist/text-scenes.js +338 -0
  63. package/dist/text-sdf.d.ts +28 -0
  64. package/dist/text-sdf.js +119 -0
  65. package/package.json +80 -4
@@ -0,0 +1,242 @@
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
+ /** Straight pieces a curve is chopped into. Eight keeps the chord error well under a texel. */
37
+ const CURVE_STEPS = 8;
38
+ /** Directions closer than this count as one continuous edge, so they share a channel. */
39
+ const COLLINEAR_COSINE = 0.9986;
40
+ function unit(dx, dy) {
41
+ const length = Math.hypot(dx, dy);
42
+ return length === 0 ? [0, 0] : [dx / length, dy / length];
43
+ }
44
+ /** Tangent leaving a segment's start, which is where its colour has to agree with the previous one. */
45
+ function outgoingDirection(segment) {
46
+ const reference = segment.control1 ?? segment.to;
47
+ return unit(reference[0] - segment.from[0], reference[1] - segment.from[1]);
48
+ }
49
+ /** Tangent arriving at a segment's end, the other half of the join test. */
50
+ function incomingDirection(segment) {
51
+ const reference = segment.control2 ?? segment.control1 ?? segment.from;
52
+ return unit(segment.to[0] - reference[0], segment.to[1] - reference[1]);
53
+ }
54
+ function continuesEdge(previous, next) {
55
+ const incoming = incomingDirection(previous);
56
+ const outgoing = outgoingDirection(next);
57
+ return incoming[0] * outgoing[0] + incoming[1] * outgoing[1] >= COLLINEAR_COSINE;
58
+ }
59
+ /**
60
+ * Assigns each segment to one of three channels. Segments that continue the same edge share a
61
+ * channel; a join that actually turns the outline advances to the next one, so the three edges of a
62
+ * corner end up on three different channels.
63
+ */
64
+ export function colorOutlineEdges(segments) {
65
+ const channels = new Uint8Array(segments.length);
66
+ let channel = 0;
67
+ for (let index = 0; index < segments.length; index++) {
68
+ if (index > 0 && !continuesEdge(segments[index - 1], segments[index]))
69
+ channel = (channel + 1) % 3;
70
+ channels[index] = channel;
71
+ }
72
+ return channels;
73
+ }
74
+ function evaluate(segment, t) {
75
+ const { from, to, control1, control2 } = segment;
76
+ const u = 1 - t;
77
+ if (control1 === undefined)
78
+ return to;
79
+ if (control2 === undefined) {
80
+ return [
81
+ u * u * from[0] + 2 * u * t * control1[0] + t * t * to[0],
82
+ u * u * from[1] + 2 * u * t * control1[1] + t * t * to[1],
83
+ ];
84
+ }
85
+ return [
86
+ u * u * u * from[0] + 3 * u * u * t * control1[0] + 3 * u * t * t * control2[0] + t * t * t * to[0],
87
+ u * u * u * from[1] + 3 * u * u * t * control1[1] + 3 * u * t * t * control2[1] + t * t * t * to[1],
88
+ ];
89
+ }
90
+ /** Chops curves into straight pieces, each inheriting its parent's channel. */
91
+ export function flattenOutline(segments, channels, steps = CURVE_STEPS) {
92
+ const lines = [];
93
+ for (let index = 0; index < segments.length; index++) {
94
+ const segment = segments[index];
95
+ const channel = ((channels[index] ?? 0) % 3);
96
+ if (segment.control1 === undefined) {
97
+ lines.push({ from: segment.from, to: segment.to, channel });
98
+ continue;
99
+ }
100
+ for (let step = 0; step < steps; step++) {
101
+ lines.push({ from: evaluate(segment, step / steps), to: evaluate(segment, (step + 1) / steps), channel });
102
+ }
103
+ }
104
+ return lines;
105
+ }
106
+ /** Even-odd containment: a horizontal ray from the point, counting crossings to its right. */
107
+ function isInside(px, py, lines) {
108
+ let inside = false;
109
+ for (const line of lines) {
110
+ const [ax, ay] = line.from;
111
+ const [bx, by] = line.to;
112
+ if (ay > py !== by > py) {
113
+ const crossing = ax + ((py - ay) / (by - ay)) * (bx - ax);
114
+ if (px < crossing)
115
+ inside = !inside;
116
+ }
117
+ }
118
+ return inside;
119
+ }
120
+ function distanceToLine(px, py, line) {
121
+ const [ax, ay] = line.from;
122
+ const [bx, by] = line.to;
123
+ const dx = bx - ax;
124
+ const dy = by - ay;
125
+ const lengthSquared = dx * dx + dy * dy;
126
+ if (lengthSquared === 0)
127
+ return Math.hypot(px - ax, py - ay);
128
+ const projected = ((px - ax) * dx + (py - ay) * dy) / lengthSquared;
129
+ const t = projected < 0 ? 0 : projected > 1 ? 1 : projected;
130
+ return Math.hypot(px - (ax + t * dx), py - (ay + t * dy));
131
+ }
132
+ /** Encodes one distance as `distance / (2 × range) + 0.5`, saturated to an 8-bit channel. */
133
+ function encodeDistance(value, range) {
134
+ const encoded = Math.round((value / (2 * range) + 0.5) * 255);
135
+ return encoded < 0 ? 0 : encoded > 255 ? 255 : encoded;
136
+ }
137
+ /** Indices of three channel values in ascending order. */
138
+ function ascending(values) {
139
+ const order = [0, 1, 2];
140
+ if (values[order[0]] > values[order[1]])
141
+ [order[0], order[1]] = [order[1], order[0]];
142
+ if (values[order[1]] > values[order[2]])
143
+ [order[1], order[2]] = [order[2], order[1]];
144
+ if (values[order[0]] > values[order[1]])
145
+ [order[0], order[1]] = [order[1], order[0]];
146
+ return order;
147
+ }
148
+ /**
149
+ * How far a texel's median may sit from the exact shape distance before it is corrected, in texels.
150
+ * Half a texel is the point where the reconstruction visibly shifts the edge.
151
+ */
152
+ const CORRECTION_TOLERANCE = 0.5;
153
+ /**
154
+ * Moves each texel's median onto the exact shape distance, in place.
155
+ *
156
+ * Only the channel or channels required to make that distance the middle value are written; the
157
+ * remaining channel keeps its own gradient, which is what stops a magnified edge from flattening
158
+ * across the corrected texel. Beyond the encoded band nothing is touched: there the coverage ramp
159
+ * is already saturated, so the median's exact value cannot move a pixel.
160
+ */
161
+ function correctMedians(texels, distances, range) {
162
+ const tolerance = (CORRECTION_TOLERANCE / (2 * range)) * 255;
163
+ for (let index = 0; index < distances.length; index++) {
164
+ const shape = distances[index];
165
+ if (!(Math.abs(shape) <= range))
166
+ continue;
167
+ const base = index * 4;
168
+ const red = texels[base];
169
+ const green = texels[base + 1];
170
+ const blue = texels[base + 2];
171
+ const order = ascending([red, green, blue]);
172
+ const values = [red, green, blue];
173
+ const lowest = values[order[0]];
174
+ const median = values[order[1]];
175
+ const highest = values[order[2]];
176
+ const target = encodeDistance(shape, range);
177
+ if (Math.abs(median - target) <= tolerance)
178
+ continue;
179
+ if (target < median) {
180
+ // Lower the highest channel. Once the target is at or below the lowest one, that channel has
181
+ // to move as well, or it would become the new median instead.
182
+ texels[base + order[2]] = target;
183
+ if (target <= lowest)
184
+ texels[base + order[0]] = target;
185
+ }
186
+ else {
187
+ texels[base + order[0]] = target;
188
+ if (target >= highest)
189
+ texels[base + order[2]] = target;
190
+ }
191
+ }
192
+ }
193
+ /**
194
+ * Renders one glyph outline into an RGBA8 distance field.
195
+ *
196
+ * Each channel holds the signed distance to the nearest edge in its group, averaged over the texel
197
+ * centre, and encoded as `distance / (2 × range) + 0.5` so 128 is the edge. Channels no edge was
198
+ * assigned to saturate, because a median over two real distances and one saturated value still
199
+ * resolves to a real edge. Alpha carries the *true* distance to the shape, which is msdfgen's
200
+ * `mtsdf` layout: the median in RGB preserves the corners a single channel rounds off, while a
201
+ * uniform-width outline reads alpha, because a per-edge median dents exactly at a corner. The field
202
+ * is then corrected against the exact shape distance — see the error-correction note at the top of
203
+ * this file.
204
+ */
205
+ export function msdfFromOutline(segments, width, height, range, steps = CURVE_STEPS) {
206
+ if (!(range > 0))
207
+ throw new RangeError("msdf range must be greater than zero");
208
+ const lines = flattenOutline(segments, colorOutlineEdges(segments), steps);
209
+ const texels = new Uint8Array(width * height * 4);
210
+ const distances = new Float64Array(width * height);
211
+ for (let y = 0; y < height; y++) {
212
+ for (let x = 0; x < width; x++) {
213
+ const px = x + 0.5;
214
+ const py = y + 0.5;
215
+ const inside = isInside(px, py, lines);
216
+ const nearest = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
217
+ const signed = [0, 0, 0];
218
+ let shapeDistance = Number.POSITIVE_INFINITY;
219
+ for (const line of lines) {
220
+ const distance = distanceToLine(px, py, line);
221
+ if (distance < shapeDistance)
222
+ shapeDistance = distance;
223
+ if (distance < nearest[line.channel]) {
224
+ nearest[line.channel] = distance;
225
+ signed[line.channel] = inside ? distance : -distance;
226
+ }
227
+ }
228
+ const index = y * width + x;
229
+ const target = index * 4;
230
+ for (let channel = 0; channel < 3; channel++) {
231
+ const value = nearest[channel] === Number.POSITIVE_INFINITY ? (inside ? range : -range) : signed[channel];
232
+ texels[target + channel] = encodeDistance(value, range);
233
+ }
234
+ const shape = inside ? shapeDistance : -shapeDistance;
235
+ // Alpha holds the true distance, so the shader can draw an outline that keeps its width.
236
+ texels[target + 3] = encodeDistance(shape, range);
237
+ distances[index] = shape;
238
+ }
239
+ }
240
+ correctMedians(texels, distances, range);
241
+ return texels;
242
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Raster-size selection for the on-demand glyph atlas.
3
+ *
4
+ * reference keys a `FontAtlas` by font size and rasterises each size at the physical pixel size it will be
5
+ * drawn at, so every font size comes out sharp and every font size pays for its own atlas. A debug
6
+ * draw frame cannot afford that shape: its text changes size continuously (a world block follows the
7
+ * camera) and a CJK glyph set is large, so one atlas per size would re-rasterise the same character
8
+ * over and over and grow without any eviction.
9
+ *
10
+ * The fix is the one browsers use for their glyph cache: rasterise at the *screen* size the glyph
11
+ * will occupy, quantised onto a fixed ladder of sizes. A size between two rungs takes the rung above
12
+ * it — at most `step`× oversampled, which the mip chain covers when the block is drawn smaller — so
13
+ * the glyphs the atlas holds stay bounded while the size keeps moving.
14
+ */
15
+ /** Ratio between neighbouring rungs. 1.25 keeps the worst-case oversampling at a quarter of a texel. */
16
+ export declare const DEFAULT_RASTER_STEP = 1.25;
17
+ /** Smallest and largest raster rungs, in texels along the font size. Below `MIN_RASTER_PX` a stem is
18
+ * thinner than a texel; above `MAX_RASTER_PX` a glyph costs more atlas area than it can pay for. */
19
+ export declare const MIN_RASTER_PX = 6;
20
+ export declare const MAX_RASTER_PX = 256;
21
+ /**
22
+ * The smallest raster size on the ladder that covers `screenPx`.
23
+ *
24
+ * Rounding up rather than to the nearest rung is what keeps the atlas an upper bound on what the
25
+ * screen asks for: a glyph is never magnified when it is decoded, only minified through the mip
26
+ * chain, which is the direction that still looks sharp.
27
+ */
28
+ export declare function quantizeRasterPx(screenPx: number, step?: number, min?: number, max?: number): number;
29
+ /** The raster scale a glyph of `fontSize` layout pixels needs in order to land on `rasterPx` texels. */
30
+ export declare function rasterScaleFor(rasterPx: number, fontSize: number): number;
31
+ /**
32
+ * Screen pixels one world unit covers at `point`, for the perspective and orthographic matrices the
33
+ * camera writes.
34
+ *
35
+ * This is the same estimate the world-width shaders use (`viewProjection[1][1] * viewportSize.y *
36
+ * 0.5 / clip.w`), so a raster size picked here agrees with the width the shaders compute for the
37
+ * block. It ignores how the block's own plane is angled to the camera, which only matters for the
38
+ * rung chosen; `quantizeRasterPx` absorbs a miss of one rung by oversampling.
39
+ */
40
+ export declare function screenPixelsPerWorldUnit(viewProjection: ArrayLike<number>, point: readonly [number, number, number], viewportHeight: number): number;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Raster-size selection for the on-demand glyph atlas.
3
+ *
4
+ * reference keys a `FontAtlas` by font size and rasterises each size at the physical pixel size it will be
5
+ * drawn at, so every font size comes out sharp and every font size pays for its own atlas. A debug
6
+ * draw frame cannot afford that shape: its text changes size continuously (a world block follows the
7
+ * camera) and a CJK glyph set is large, so one atlas per size would re-rasterise the same character
8
+ * over and over and grow without any eviction.
9
+ *
10
+ * The fix is the one browsers use for their glyph cache: rasterise at the *screen* size the glyph
11
+ * will occupy, quantised onto a fixed ladder of sizes. A size between two rungs takes the rung above
12
+ * it — at most `step`× oversampled, which the mip chain covers when the block is drawn smaller — so
13
+ * the glyphs the atlas holds stay bounded while the size keeps moving.
14
+ */
15
+ /** Ratio between neighbouring rungs. 1.25 keeps the worst-case oversampling at a quarter of a texel. */
16
+ export const DEFAULT_RASTER_STEP = 1.25;
17
+ /** Smallest and largest raster rungs, in texels along the font size. Below `MIN_RASTER_PX` a stem is
18
+ * thinner than a texel; above `MAX_RASTER_PX` a glyph costs more atlas area than it can pay for. */
19
+ export const MIN_RASTER_PX = 6;
20
+ export const MAX_RASTER_PX = 256;
21
+ /**
22
+ * The smallest raster size on the ladder that covers `screenPx`.
23
+ *
24
+ * Rounding up rather than to the nearest rung is what keeps the atlas an upper bound on what the
25
+ * screen asks for: a glyph is never magnified when it is decoded, only minified through the mip
26
+ * chain, which is the direction that still looks sharp.
27
+ */
28
+ export function quantizeRasterPx(screenPx, step = DEFAULT_RASTER_STEP, min = MIN_RASTER_PX, max = MAX_RASTER_PX) {
29
+ if (Number.isNaN(screenPx) || !(screenPx > min))
30
+ return min;
31
+ if (screenPx >= max)
32
+ return max;
33
+ if (!(step > 1))
34
+ return Math.min(max, Math.ceil(screenPx));
35
+ const exponent = Math.ceil(Math.log(screenPx / min) / Math.log(step));
36
+ return Math.min(max, Math.max(min, Math.ceil(min * Math.pow(step, exponent))));
37
+ }
38
+ /** The raster scale a glyph of `fontSize` layout pixels needs in order to land on `rasterPx` texels. */
39
+ export function rasterScaleFor(rasterPx, fontSize) {
40
+ return fontSize > 0 ? rasterPx / fontSize : 1;
41
+ }
42
+ /**
43
+ * Screen pixels one world unit covers at `point`, for the perspective and orthographic matrices the
44
+ * camera writes.
45
+ *
46
+ * This is the same estimate the world-width shaders use (`viewProjection[1][1] * viewportSize.y *
47
+ * 0.5 / clip.w`), so a raster size picked here agrees with the width the shaders compute for the
48
+ * block. It ignores how the block's own plane is angled to the camera, which only matters for the
49
+ * rung chosen; `quantizeRasterPx` absorbs a miss of one rung by oversampling.
50
+ */
51
+ export function screenPixelsPerWorldUnit(viewProjection, point, viewportHeight) {
52
+ const [x, y, z] = point;
53
+ const clipW = viewProjection[3] * x + viewProjection[7] * y + viewProjection[11] * z + viewProjection[15];
54
+ // Behind or on the near plane the projection has no finite scale; fall back to one pixel per unit
55
+ // rather than returning a value that would send the raster ladder to its top rung.
56
+ if (!(clipW > 1e-4))
57
+ return 1;
58
+ const scale = Math.abs(viewProjection[5]) * viewportHeight * 0.5 / clipW;
59
+ return Number.isFinite(scale) && scale > 0 ? scale : 1;
60
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Screen-space text pages.
3
+ *
4
+ * These are vgpu's own capability rather than a port — the reference has no text API — so they
5
+ * are organised as **one page per question**:
6
+ *
7
+ * - `text-encodings` — the same text from every glyph source, at two sizes, plus the outline pair.
8
+ * - `text-scale` — the same sentence at six sizes, over three raster strategies at one size.
9
+ * - `text-world-zoom` — world-space rows meant to be magnified with the camera, which is the only way
10
+ * to see what a source does once a block outgrows the raster its glyphs were built at.
11
+ * - `text-layout` — wrapping, justification, bounds, line height, letter spacing and spans.
12
+ * - `text-anchor` — the nine anchors against the marker they land on and their own bounds.
13
+ * - `text-cjk` — CJK wrapping, kinsoku and mixed-script runs.
14
+ *
15
+ * Three conventions run through all of them, because a comparison without them is unreadable:
16
+ *
17
+ * 1. **One sentence, repeated per condition**, with the condition labelled above the column. A page
18
+ * whose rows each say something different cannot be compared at all.
19
+ * 2. **Every caption stays inside the width of its column**, which is the mistake this layout makes
20
+ * first: overlapping captions are worse than a shorter label.
21
+ * 3. **A 1 px rule** in the bottom-left corner of every screen-space page, so an edge has something of
22
+ * a known width next to it. It is drawn with one-pixel **points**, because `recorder.line` has no
23
+ * screen-space mode: a line is a world-space segment projected through the camera, which lands as
24
+ * an arbitrary diagonal on a page whose content is in pixels.
25
+ *
26
+ * Text blocks deliberately name no glyph source: they take the default one, which rasterises each
27
+ * glyph at the size it covers on screen. That is what a caller gets out of the box, and the thing
28
+ * these pages exist to show; a block that wants a fixed raster says so explicitly.
29
+ *
30
+ * A page that mixes screen-space captions with world-space content cannot work: the camera is framed
31
+ * around every command's position, so a caption at `[24, 520]` drags the viewport to those world
32
+ * coordinates and the real content ends up off-screen. `text-world-zoom` is therefore world-space end
33
+ * to end, captions included.
34
+ */
35
+ import type { FrameRecorder } from "./frame-recorder.js";
36
+ /**
37
+ * `text-encodings` — the same text from four sources.
38
+ *
39
+ * Two bands per column: the word at a normal size, and a shorter run of the same ink at more than
40
+ * twice that, which is where a coverage source and a distance field stop looking alike. The outline
41
+ * pair belongs on this page rather than its own: how a ring is obtained is a property of the encoding.
42
+ */
43
+ export declare function textEncodingsScene(recorder: FrameRecorder): void;
44
+ /**
45
+ * `text-scale` — three raster strategies at one size, then one size per row.
46
+ *
47
+ * The top row is the comparison the screen-driven ladder exists for: the same 26 px text under the
48
+ * ladder, a fixed 1× raster and a fixed 4× raster. Below it the sizes run from 8 px to 96 px, so the
49
+ * rung changes — and the absence of a soft step between them — are visible in one frame.
50
+ */
51
+ export declare function textScaleScene(recorder: FrameRecorder): void;
52
+ /**
53
+ * `text-world-zoom` — rows meant to be magnified.
54
+ *
55
+ * Screen-space text cannot be magnified: its pixel size is fixed, so a bigger block is simply a
56
+ * request for a new rasterisation. World-space text is where the camera does the magnifying, which is
57
+ * the only honest test of what a raster does past its own size — a coverage source runs into the
58
+ * texels it stored, a distance field keeps rebuilding the edge from them. The whole page is world
59
+ * space, captions included, so the framing sees one coordinate system.
60
+ */
61
+ export declare function textWorldZoomScene(recorder: FrameRecorder): void;
62
+ /**
63
+ * `text-layout` — what the layout can do, one row per capability.
64
+ *
65
+ * Every row is labelled on the left, and every bounded row draws the box it wrapped into: a wrapping
66
+ * rule is unreadable without the box it wrapped against.
67
+ */
68
+ export declare function textLayoutScene(recorder: FrameRecorder): void;
69
+ /**
70
+ * `text-anchor` — the nine anchors against the marker they land on.
71
+ *
72
+ * The `+` marks the position the block is given and the box is the block's own bounds, so both of the
73
+ * things an anchor moves between are visible at once. Captions sit above the cell rather than beside
74
+ * the box, which is what keeps them from being drawn over.
75
+ */
76
+ export declare function textAnchorScene(recorder: FrameRecorder): void;
77
+ /**
78
+ * `text-cjk` — wrapping and kinsoku, which no latin page can show.
79
+ *
80
+ * CJK has no spaces to break at, so the wrap is per character, and kinsoku is what keeps a closing
81
+ * mark off the start of a line and an opening bracket off its end.
82
+ */
83
+ export declare function textCjkScene(recorder: FrameRecorder): void;
84
+ /** Dispatches one of the text pages by scene id. */
85
+ export declare function textScene(id: string, recorder: FrameRecorder): void;