vgpu-render 0.1.0 → 0.3.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 +101 -34
- package/dist/curve.d.ts +26 -1
- package/dist/curve.js +53 -0
- package/dist/debug-draw-3d.d.ts +3 -3
- package/dist/debug-draw-3d.js +10 -10
- package/dist/frame-draw-list-2d.d.ts +286 -0
- package/dist/frame-draw-list-2d.js +668 -0
- 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 +11 -2
- package/dist/geometry-generators.js +37 -1
- package/dist/gpu-debug-draw-2d.d.ts +1 -1
- package/dist/gpu-debug-draw-2d.js +10 -2
- 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 +295 -7
- 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/frame-recorder-2d.d.ts +0 -177
- package/dist/frame-recorder-2d.js +0 -378
- 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/shape2d-shader.js
CHANGED
|
@@ -59,6 +59,8 @@ struct In {
|
|
|
59
59
|
@location(12) color: vec4f,
|
|
60
60
|
/** bit0: filled; bits 8..15: polygon vertex count. */
|
|
61
61
|
@location(13) flags: u32,
|
|
62
|
+
/** (visible, gap, phase) of a dashed outline in shape-space units; gap 0 disables it. */
|
|
63
|
+
@location(14) dash: vec3f,
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
struct Out {
|
|
@@ -75,6 +77,7 @@ struct Out {
|
|
|
75
77
|
@location(7) rotation: f32,
|
|
76
78
|
@location(8) @interpolate(flat, either) sides: u32,
|
|
77
79
|
@location(9) @interpolate(flat, either) flags: u32,
|
|
80
|
+
@location(10) dash: vec3f,
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
fn rotate2(p: vec2f, angle: f32) -> vec2f {
|
|
@@ -171,12 +174,17 @@ fn sd_rhombus(p: vec2f, a: f32, b: f32) -> f32 {
|
|
|
171
174
|
}
|
|
172
175
|
|
|
173
176
|
/** Sector of at most half a turn: the intersection of the disc and the two edge half-planes. */
|
|
174
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Angular wedge of sweep radians from start: only the two bounding half-planes, no radius.
|
|
179
|
+
* Keeping the disc out of it matters for the complementary wedge below, where a radius term would
|
|
180
|
+
* clamp the opposite side to zero and leave the uncovered wedge of an arc or sector painted.
|
|
181
|
+
*/
|
|
182
|
+
fn sd_wedge(p: vec2f, start: f32, sweep: f32) -> f32 {
|
|
175
183
|
let end = start + sweep;
|
|
176
184
|
// Inward normals of the two straight edges, in the order the sweep turns.
|
|
177
185
|
let startEdge = vec2f(sin(start), -cos(start));
|
|
178
186
|
let endEdge = vec2f(-sin(end), cos(end));
|
|
179
|
-
return max(
|
|
187
|
+
return max(dot(p, startEdge), dot(p, endEdge));
|
|
180
188
|
}
|
|
181
189
|
|
|
182
190
|
/**
|
|
@@ -186,9 +194,9 @@ fn sd_convex_sector(p: vec2f, radius: f32, start: f32, sweep: f32) -> f32 {
|
|
|
186
194
|
fn sd_sector(p: vec2f, radius: f32, start: f32, sweep: f32) -> f32 {
|
|
187
195
|
let disc = length(p) - radius;
|
|
188
196
|
if (abs(sweep) >= TAU - EPSILON) { return disc; }
|
|
189
|
-
if (abs(sweep) <= PI) { return max(disc,
|
|
197
|
+
if (abs(sweep) <= PI) { return max(disc, sd_wedge(p, start, sweep)); }
|
|
190
198
|
let complementSweep = select(TAU + sweep, TAU - sweep, sweep > 0.0);
|
|
191
|
-
let complement =
|
|
199
|
+
let complement = sd_wedge(p, start + sweep, complementSweep);
|
|
192
200
|
return max(disc, -complement);
|
|
193
201
|
}
|
|
194
202
|
|
|
@@ -213,8 +221,9 @@ fn shape_distance(shape: u32, p: vec2f, size: vec2f, vertexB: vec2f, vertexC: ve
|
|
|
213
221
|
case SHAPE_ROUNDED_RECT: { return sd_rounded_box(p, size, cornerRadius); }
|
|
214
222
|
case SHAPE_CIRCLE: { return sd_ellipse(p, size); }
|
|
215
223
|
case SHAPE_ELLIPSE: { return sd_ellipse(p, size); }
|
|
216
|
-
// An arc has no interior:
|
|
217
|
-
|
|
224
|
+
// An arc has no interior: the ring alone ignores the angles, so it is clipped to the swept
|
|
225
|
+
// wedge. Filling one still yields a hairline, because the distance stays unsigned.
|
|
226
|
+
case SHAPE_ARC: { return max(abs(length(p) - size.x), sd_sector(p, size.x, startAngle, sweep)); }
|
|
218
227
|
case SHAPE_ANNULUS: { return sd_annulus(p, size.y, size.x); }
|
|
219
228
|
case SHAPE_CAPSULE: { return sd_capsule(p, size.y, size.x); }
|
|
220
229
|
case SHAPE_POLYGON: { return sd_regular_polygon(p, size.x, sides); }
|
|
@@ -226,6 +235,256 @@ fn shape_distance(shape: u32, p: vec2f, size: vec2f, vertexB: vec2f, vertexC: ve
|
|
|
226
235
|
}
|
|
227
236
|
}
|
|
228
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Perimeter parameter of the point on a rectangle's outline nearest to the sample, measured
|
|
240
|
+
* anticlockwise from the (sx, sy) corner. The stroke only touches pixels within a pixel or two
|
|
241
|
+
* of the outline, so picking the closer of the two candidate edges there is exact.
|
|
242
|
+
*/
|
|
243
|
+
fn rect_perimeter(p: vec2f, halfSize: vec2f) -> f32 {
|
|
244
|
+
let sx = halfSize.x;
|
|
245
|
+
let sy = halfSize.y;
|
|
246
|
+
let q = clamp(p, -halfSize, halfSize);
|
|
247
|
+
let overX = abs(p.x) - sx;
|
|
248
|
+
let overY = abs(p.y) - sy;
|
|
249
|
+
if (overY >= overX) {
|
|
250
|
+
if (p.y >= 0.0) { return sx - q.x; }
|
|
251
|
+
return 2.0 * sx + 2.0 * sy + (sx + q.x);
|
|
252
|
+
}
|
|
253
|
+
if (p.x < 0.0) { return 2.0 * sx + (sy - q.y); }
|
|
254
|
+
return 4.0 * sx + 2.0 * sy + (q.y + sy);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Closed-outline length in shape-space units, or -1 for an open outline (an arc). A closed
|
|
259
|
+
* outline snaps its dash period to a whole number of periods, so the pattern meets itself at the
|
|
260
|
+
* perimeter start instead of jumping by P mod period.
|
|
261
|
+
*/
|
|
262
|
+
fn perimeter_total(shape: u32, size: vec2f, vertexB: vec2f, vertexC: vec2f, params: vec4f, sides: u32) -> f32 {
|
|
263
|
+
switch shape {
|
|
264
|
+
case SHAPE_RECT: { return 4.0 * (size.x + size.y); }
|
|
265
|
+
case SHAPE_ROUNDED_RECT: {
|
|
266
|
+
let corner = clamp(params.x, 0.0, min(size.x, size.y));
|
|
267
|
+
let inner = max(size - vec2f(corner), vec2f(0.0));
|
|
268
|
+
return 4.0 * (inner.x + inner.y) + 2.0 * PI * corner;
|
|
269
|
+
}
|
|
270
|
+
case SHAPE_CIRCLE: { return TAU * size.x; }
|
|
271
|
+
case SHAPE_ELLIPSE: { return ellipse_arc(TAU, size); }
|
|
272
|
+
case SHAPE_ARC: { return -1.0; }
|
|
273
|
+
case SHAPE_ANNULUS: { return TAU * (size.x + size.y); }
|
|
274
|
+
case SHAPE_CAPSULE: { return 2.0 * PI * size.x + 4.0 * size.y; }
|
|
275
|
+
case SHAPE_POLYGON: {
|
|
276
|
+
let count = max(sides, 3u);
|
|
277
|
+
return 2.0 * f32(count) * size.x * sin(PI / f32(count));
|
|
278
|
+
}
|
|
279
|
+
case SHAPE_TRIANGLE: {
|
|
280
|
+
return length(vertexB - size) + length(vertexC - vertexB) + length(size - vertexC);
|
|
281
|
+
}
|
|
282
|
+
case SHAPE_RHOMBUS: { return 4.0 * length(vec2f(0.0, size.y) - vec2f(size.x, 0.0)); }
|
|
283
|
+
case SHAPE_SECTOR: { return abs(params.w) * size.x + 2.0 * size.x; }
|
|
284
|
+
case SHAPE_SEGMENT: {
|
|
285
|
+
let sweepAbs = abs(params.w);
|
|
286
|
+
return sweepAbs * size.x + 2.0 * size.x * sin(sweepAbs * 0.5);
|
|
287
|
+
}
|
|
288
|
+
default: { return -1.0; }
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Wraps an angle into [0, TAU). */
|
|
293
|
+
fn wrap_tau(angle: f32) -> f32 {
|
|
294
|
+
let wrapped = angle % TAU;
|
|
295
|
+
return select(wrapped + TAU, wrapped, wrapped >= 0.0);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Distance from the sample to the segment a-b, and how far along that segment it sits. */
|
|
299
|
+
fn nearest_edge(p: vec2f, a: vec2f, b: vec2f) -> vec2f {
|
|
300
|
+
let edge = b - a;
|
|
301
|
+
let along = clamp(dot(p - a, edge) / max(dot(edge, edge), EPSILON), 0.0, 1.0);
|
|
302
|
+
return vec2f(length(p - (a + edge * along)), along * length(edge));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Arc length of an ellipse from angle 0 to theta, Simpson-integrated in 12 steps. The ellipse
|
|
307
|
+
* has no elementary arc-length formula, so this is the one shape whose dash spacing is an
|
|
308
|
+
* approximation rather than a closed form.
|
|
309
|
+
*/
|
|
310
|
+
fn ellipse_arc(theta: f32, radii: vec2f) -> f32 {
|
|
311
|
+
let steps = 12u;
|
|
312
|
+
let h = theta / f32(steps);
|
|
313
|
+
var sum = 0.0;
|
|
314
|
+
for (var i = 0u; i <= steps; i += 1u) {
|
|
315
|
+
let a = h * f32(i);
|
|
316
|
+
let speed = sqrt(radii.x * radii.x * sin(a) * sin(a) + radii.y * radii.y * cos(a) * cos(a));
|
|
317
|
+
var weight = 2.0;
|
|
318
|
+
if (i == 0u || i == steps) { weight = 1.0; }
|
|
319
|
+
else if (i % 2u == 1u) { weight = 4.0; }
|
|
320
|
+
sum = sum + weight * speed;
|
|
321
|
+
}
|
|
322
|
+
return sum * h / 3.0;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Rounded rectangle: four straight edges and four quarter arcs, from the top-right corner. */
|
|
326
|
+
fn rounded_rect_perimeter(p: vec2f, halfSize: vec2f, cornerRadius: f32) -> f32 {
|
|
327
|
+
let corner = clamp(cornerRadius, 0.0, min(halfSize.x, halfSize.y));
|
|
328
|
+
let inner = max(halfSize - vec2f(corner), vec2f(0.0));
|
|
329
|
+
let center = clamp(p, -inner, inner);
|
|
330
|
+
let delta = p - center;
|
|
331
|
+
let straightX = 2.0 * inner.x;
|
|
332
|
+
let straightY = 2.0 * inner.y;
|
|
333
|
+
let quarter = corner * PI * 0.5;
|
|
334
|
+
let leftBase = straightX + quarter;
|
|
335
|
+
let bottomBase = straightX + 2.0 * quarter + straightY;
|
|
336
|
+
// Right edge starts after the bottom-right quarter arc: two straight runs, three quarter
|
|
337
|
+
// arcs and one straight height have already been walked.
|
|
338
|
+
let rightBase = 2.0 * straightX + 3.0 * quarter + straightY;
|
|
339
|
+
let topRightBase = 2.0 * straightX + 2.0 * straightY + 3.0 * quarter;
|
|
340
|
+
if (abs(delta.x) > EPSILON && abs(delta.y) > EPSILON) {
|
|
341
|
+
let phi = wrap_tau(atan2(delta.y, delta.x));
|
|
342
|
+
if (phi < PI * 0.5) { return topRightBase + phi * corner; }
|
|
343
|
+
if (phi < PI) { return straightX + (phi - PI * 0.5) * corner; }
|
|
344
|
+
if (phi < PI * 1.5) { return leftBase + straightY + (phi - PI) * corner; }
|
|
345
|
+
return bottomBase + straightX + (phi - PI * 1.5) * corner;
|
|
346
|
+
}
|
|
347
|
+
if (abs(delta.y) > EPSILON) {
|
|
348
|
+
if (delta.y > 0.0) { return inner.x - p.x; }
|
|
349
|
+
return bottomBase + (p.x + inner.x);
|
|
350
|
+
}
|
|
351
|
+
if (delta.x > 0.0) { return rightBase + (p.y + inner.y); }
|
|
352
|
+
return leftBase + (inner.y - p.y);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Capsule: upper cap, left edge, lower cap, right edge, from the upper-right cap start. */
|
|
356
|
+
fn capsule_perimeter(p: vec2f, radius: f32, halfLength: f32) -> f32 {
|
|
357
|
+
let arc = PI * radius;
|
|
358
|
+
if (p.y >= halfLength) {
|
|
359
|
+
return clamp(atan2(p.y - halfLength, p.x), 0.0, PI) * radius;
|
|
360
|
+
}
|
|
361
|
+
if (p.y <= -halfLength) {
|
|
362
|
+
let angle = clamp(wrap_tau(atan2(p.y + halfLength, p.x)), PI, TAU);
|
|
363
|
+
return arc + 2.0 * halfLength + (angle - PI) * radius;
|
|
364
|
+
}
|
|
365
|
+
if (p.x >= 0.0) { return 2.0 * arc + 2.0 * halfLength + (p.y + halfLength); }
|
|
366
|
+
return arc + (halfLength - p.y);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Annulus: outer ring forwards, then inner ring backwards, so the band stays continuous. */
|
|
370
|
+
fn annulus_perimeter(p: vec2f, outer: f32, inner: f32) -> f32 {
|
|
371
|
+
let radius = length(p);
|
|
372
|
+
let angle = wrap_tau(atan2(p.y, p.x));
|
|
373
|
+
if (abs(radius - outer) <= abs(radius - inner)) { return angle * outer; }
|
|
374
|
+
return TAU * outer + (TAU - angle) * inner;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Arc: only the swept wedge has a perimeter, so samples past an end clamp to that end. */
|
|
378
|
+
fn arc_perimeter(p: vec2f, radius: f32, start: f32, sweep: f32) -> f32 {
|
|
379
|
+
let sweepAbs = abs(sweep);
|
|
380
|
+
let angle = wrap_tau(atan2(p.y, p.x));
|
|
381
|
+
var offset = wrap_tau(angle - start);
|
|
382
|
+
if (sweep < 0.0) { offset = wrap_tau(start - angle); }
|
|
383
|
+
return min(offset, sweepAbs) * radius;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Sector: arc, then the radius at the arc end, then the radius back to the arc start. */
|
|
387
|
+
fn sector_perimeter(p: vec2f, radius: f32, start: f32, sweep: f32) -> f32 {
|
|
388
|
+
let sweepAbs = abs(sweep);
|
|
389
|
+
let angle = wrap_tau(atan2(p.y, p.x));
|
|
390
|
+
var offset = wrap_tau(angle - start);
|
|
391
|
+
if (sweep < 0.0) { offset = wrap_tau(start - angle); }
|
|
392
|
+
let startDir = vec2f(cos(start), sin(start)) * radius;
|
|
393
|
+
let endDir = vec2f(cos(start + sweep), sin(start + sweep)) * radius;
|
|
394
|
+
let startEdge = nearest_edge(p, vec2f(0.0), startDir);
|
|
395
|
+
let endEdge = nearest_edge(p, vec2f(0.0), endDir);
|
|
396
|
+
let arcLength = sweepAbs * radius;
|
|
397
|
+
let arcDistance = select(abs(length(p) - radius), min(length(p - startDir), length(p - endDir)), offset > sweepAbs);
|
|
398
|
+
if (arcDistance <= startEdge.x && arcDistance <= endEdge.x) {
|
|
399
|
+
return min(offset, sweepAbs) * radius;
|
|
400
|
+
}
|
|
401
|
+
if (endEdge.x <= startEdge.x) { return arcLength + (radius - endEdge.y); }
|
|
402
|
+
return arcLength + radius + startEdge.y;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Segment: the arc, then the chord from the arc end back to its start. */
|
|
406
|
+
fn segment_perimeter(p: vec2f, radius: f32, start: f32, sweep: f32) -> f32 {
|
|
407
|
+
let sweepAbs = abs(sweep);
|
|
408
|
+
let angle = wrap_tau(atan2(p.y, p.x));
|
|
409
|
+
var offset = wrap_tau(angle - start);
|
|
410
|
+
if (sweep < 0.0) { offset = wrap_tau(start - angle); }
|
|
411
|
+
let arcStart = vec2f(cos(start), sin(start)) * radius;
|
|
412
|
+
let arcEnd = vec2f(cos(start + sweep), sin(start + sweep)) * radius;
|
|
413
|
+
let chord = nearest_edge(p, arcEnd, arcStart);
|
|
414
|
+
let arcLength = sweepAbs * radius;
|
|
415
|
+
let arcDistance = select(abs(length(p) - radius), min(length(p - arcStart), length(p - arcEnd)), offset > sweepAbs);
|
|
416
|
+
if (arcDistance <= chord.x) { return min(offset, sweepAbs) * radius; }
|
|
417
|
+
return arcLength + chord.y;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Regular polygon: pick the angular sector, then the projection onto that edge. */
|
|
421
|
+
fn polygon_perimeter(p: vec2f, radius: f32, sides: u32) -> f32 {
|
|
422
|
+
let count = max(sides, 3u);
|
|
423
|
+
let step = TAU / f32(count);
|
|
424
|
+
let edgeLength = 2.0 * radius * sin(PI / f32(count));
|
|
425
|
+
let angle = wrap_tau(atan2(p.y, p.x));
|
|
426
|
+
let index = min(u32(floor(angle / step)), count - 1u);
|
|
427
|
+
let start = vec2f(cos(step * f32(index)), sin(step * f32(index))) * radius;
|
|
428
|
+
let end = vec2f(cos(step * f32(index + 1u)), sin(step * f32(index + 1u))) * radius;
|
|
429
|
+
return f32(index) * edgeLength + nearest_edge(p, start, end).y;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Triangle: three edges, from the first vertex. */
|
|
433
|
+
fn triangle_perimeter(p: vec2f, a: vec2f, b: vec2f, c: vec2f) -> f32 {
|
|
434
|
+
let ab = nearest_edge(p, a, b);
|
|
435
|
+
let bc = nearest_edge(p, b, c);
|
|
436
|
+
let ca = nearest_edge(p, c, a);
|
|
437
|
+
let lab = length(b - a);
|
|
438
|
+
let lbc = length(c - b);
|
|
439
|
+
if (ab.x <= bc.x && ab.x <= ca.x) { return ab.y; }
|
|
440
|
+
if (bc.x <= ca.x) { return lab + bc.y; }
|
|
441
|
+
return lab + lbc + ca.y;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** Rhombus: four edges through the four diagonal tips, from the +x tip. */
|
|
445
|
+
fn rhombus_perimeter(p: vec2f, halfWidth: f32, halfHeight: f32) -> f32 {
|
|
446
|
+
let v0 = vec2f(halfWidth, 0.0);
|
|
447
|
+
let v1 = vec2f(0.0, halfHeight);
|
|
448
|
+
let v2 = vec2f(-halfWidth, 0.0);
|
|
449
|
+
let v3 = vec2f(0.0, -halfHeight);
|
|
450
|
+
let e0 = nearest_edge(p, v0, v1);
|
|
451
|
+
let e1 = nearest_edge(p, v1, v2);
|
|
452
|
+
let e2 = nearest_edge(p, v2, v3);
|
|
453
|
+
let e3 = nearest_edge(p, v3, v0);
|
|
454
|
+
let l0 = length(v1 - v0);
|
|
455
|
+
let l1 = length(v2 - v1);
|
|
456
|
+
let l2 = length(v3 - v2);
|
|
457
|
+
if (e0.x <= e1.x && e0.x <= e2.x && e0.x <= e3.x) { return e0.y; }
|
|
458
|
+
if (e1.x <= e2.x && e1.x <= e3.x) { return l0 + e1.y; }
|
|
459
|
+
if (e2.x <= e3.x) { return l0 + l1 + e2.y; }
|
|
460
|
+
return l0 + l1 + l2 + e3.y;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Arc length along the outline at the nearest outline point, in shape-space units. Every
|
|
465
|
+
* primitive now has a closed form except the ellipse, which is integrated numerically.
|
|
466
|
+
*/
|
|
467
|
+
fn perimeter_length(shape: u32, p: vec2f, size: vec2f, vertexB: vec2f, vertexC: vec2f, params: vec4f, sides: u32) -> f32 {
|
|
468
|
+
switch shape {
|
|
469
|
+
case SHAPE_RECT: { return rect_perimeter(p, size); }
|
|
470
|
+
case SHAPE_ROUNDED_RECT: { return rounded_rect_perimeter(p, size, params.x); }
|
|
471
|
+
case SHAPE_CIRCLE: { return wrap_tau(atan2(p.y, p.x)) * size.x; }
|
|
472
|
+
case SHAPE_ELLIPSE: {
|
|
473
|
+
let theta = wrap_tau(atan2(p.y / max(size.y, EPSILON), p.x / max(size.x, EPSILON)));
|
|
474
|
+
return ellipse_arc(theta, size);
|
|
475
|
+
}
|
|
476
|
+
case SHAPE_ARC: { return arc_perimeter(p, size.x, params.z, params.w); }
|
|
477
|
+
case SHAPE_ANNULUS: { return annulus_perimeter(p, size.x, size.y); }
|
|
478
|
+
case SHAPE_CAPSULE: { return capsule_perimeter(p, size.x, size.y); }
|
|
479
|
+
case SHAPE_POLYGON: { return polygon_perimeter(p, size.x, sides); }
|
|
480
|
+
case SHAPE_TRIANGLE: { return triangle_perimeter(p, size, vertexB, vertexC); }
|
|
481
|
+
case SHAPE_RHOMBUS: { return rhombus_perimeter(p, size.x, size.y); }
|
|
482
|
+
case SHAPE_SECTOR: { return sector_perimeter(p, size.x, params.z, params.w); }
|
|
483
|
+
case SHAPE_SEGMENT: { return segment_perimeter(p, size.x, params.z, params.w); }
|
|
484
|
+
default: { return -1.0; }
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
229
488
|
fn clip_to_framebuffer(clip: vec4f) -> vec2f {
|
|
230
489
|
let ndc = clip.xy / max(clip.w, EPSILON);
|
|
231
490
|
return vec2f((ndc.x * 0.5 + 0.5) * camera.viewportSize.x, (0.5 - ndc.y * 0.5) * camera.viewportSize.y);
|
|
@@ -263,6 +522,7 @@ fn clip_to_framebuffer(clip: vec4f) -> vec2f {
|
|
|
263
522
|
out.rotation = rotation;
|
|
264
523
|
out.sides = (input.flags >> 8u) & 0xffu;
|
|
265
524
|
out.flags = input.flags;
|
|
525
|
+
out.dash = input.dash;
|
|
266
526
|
return out;
|
|
267
527
|
}
|
|
268
528
|
|
|
@@ -279,7 +539,35 @@ fn clip_to_framebuffer(clip: vec4f) -> vec2f {
|
|
|
279
539
|
// Relative to the outer edge of the stroke, so one coverage ramp serves fill and stroke alike.
|
|
280
540
|
let signedPixel = select(abs(distancePixel) - max(input.params.y, 0.0) * 0.5, distancePixel, filled);
|
|
281
541
|
let width = max(fwidth(signedPixel), EPSILON);
|
|
282
|
-
|
|
542
|
+
var coverage = clamp(0.5 - signedPixel / width, 0.0, 1.0);
|
|
543
|
+
if (coverage <= 0.0) { discard; }
|
|
544
|
+
|
|
545
|
+
// Dashed outlines: walk the perimeter parameter and cut the stroke in the gaps. The transition
|
|
546
|
+
// is spread over one canvas pixel so a dash edge is as soft as the shape's own edge.
|
|
547
|
+
if ((input.flags & 1u) == 0u && input.dash.y > 0.0) {
|
|
548
|
+
let path = perimeter_length(input.shape, local, input.size, input.vertexB, input.vertexC, input.params, input.sides);
|
|
549
|
+
if (path >= 0.0) {
|
|
550
|
+
let requested = input.dash.x + input.dash.y;
|
|
551
|
+
let total = perimeter_total(input.shape, input.size, input.vertexB, input.vertexC, input.params, input.sides);
|
|
552
|
+
var period = requested;
|
|
553
|
+
var visible = input.dash.x;
|
|
554
|
+
if (total > 0.0) {
|
|
555
|
+
// Snap the period to a whole number of turns, keeping the requested duty cycle: the
|
|
556
|
+
// closed outline then ends exactly where its dash pattern started.
|
|
557
|
+
let count = max(round(total / requested), 1.0);
|
|
558
|
+
period = total / count;
|
|
559
|
+
visible = period * (input.dash.x / requested);
|
|
560
|
+
}
|
|
561
|
+
let along = fract((path + input.dash.z) / period) * period;
|
|
562
|
+
let pixel = 1.0 / scale;
|
|
563
|
+
// Distance to the nearest pattern boundary: the dash start, the dash end, and the next
|
|
564
|
+
// dash start that fract() wraps around to.
|
|
565
|
+
let edge = min(min(along, abs(along - visible)), period - along);
|
|
566
|
+
var dashCoverage = smoothstep(0.0, pixel, edge);
|
|
567
|
+
if (along > visible) { dashCoverage = 1.0 - dashCoverage; }
|
|
568
|
+
coverage = coverage * dashCoverage;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
283
571
|
if (coverage <= 0.0) { discard; }
|
|
284
572
|
return vec4f(input.color.rgb, input.color.a * coverage);
|
|
285
573
|
}
|
package/dist/text-atlas.d.ts
CHANGED
|
@@ -9,14 +9,16 @@
|
|
|
9
9
|
* 2 px of padding; both choices are kept here, since the padding is what keeps linear
|
|
10
10
|
* sampling from bleeding between neighbours.
|
|
11
11
|
*
|
|
12
|
-
* Unlike reference the atlas is not keyed per font size: the cache key carries the
|
|
13
|
-
* instead, so one page set serves every size a frame happens to use.
|
|
14
|
-
* `
|
|
12
|
+
* Unlike reference the atlas is not keyed per font size: the cache key carries the face and size
|
|
13
|
+
* instead, so one page set serves every size a frame happens to use. The face part comes from
|
|
14
|
+
* `GlyphRasterizer.faceKey`, which is how a source states which of the axes it can actually vary.
|
|
15
|
+
* Glyphs are rasterised at `rasterScale` times their font size, which decouples the atlas' detail from the logical layout
|
|
15
16
|
* size — the same separation as resampling a font at a higher point size before scaling it down.
|
|
16
17
|
* Pages grow on demand until `maxPages` is reached — the GPU renderer pins that at one page so a
|
|
17
18
|
* single draw call covers the whole atlas — and a `revision` counter tells the renderer which of
|
|
18
19
|
* them changed since the last upload.
|
|
19
20
|
*/
|
|
21
|
+
import { type FontFace } from "./text-layout.js";
|
|
20
22
|
import type { GlyphEncoding, GlyphRasterizer } from "./text-font.js";
|
|
21
23
|
/** Page size and inter-glyph padding, matching reference minimum atlas and `GLYPH_ATLAS_PADDING`. */
|
|
22
24
|
export declare const DEFAULT_ATLAS_SIZE = 512;
|
|
@@ -111,7 +113,7 @@ export declare class GlyphAtlas {
|
|
|
111
113
|
* exactly as it did. The renderer passes the size the block covers on screen instead, so the entry
|
|
112
114
|
* that comes back is rasterised for the rung that face asked for.
|
|
113
115
|
*/
|
|
114
|
-
glyph(character: string,
|
|
116
|
+
glyph(character: string, face: FontFace, fontSize: number, rasterScale?: number): AtlasGlyph | undefined;
|
|
115
117
|
/** Drops every cached glyph. The next frame re-rasterises whatever it still draws. */
|
|
116
118
|
clear(): void;
|
|
117
119
|
}
|
package/dist/text-atlas.js
CHANGED
|
@@ -9,14 +9,16 @@
|
|
|
9
9
|
* 2 px of padding; both choices are kept here, since the padding is what keeps linear
|
|
10
10
|
* sampling from bleeding between neighbours.
|
|
11
11
|
*
|
|
12
|
-
* Unlike reference the atlas is not keyed per font size: the cache key carries the
|
|
13
|
-
* instead, so one page set serves every size a frame happens to use.
|
|
14
|
-
* `
|
|
12
|
+
* Unlike reference the atlas is not keyed per font size: the cache key carries the face and size
|
|
13
|
+
* instead, so one page set serves every size a frame happens to use. The face part comes from
|
|
14
|
+
* `GlyphRasterizer.faceKey`, which is how a source states which of the axes it can actually vary.
|
|
15
|
+
* Glyphs are rasterised at `rasterScale` times their font size, which decouples the atlas' detail from the logical layout
|
|
15
16
|
* size — the same separation as resampling a font at a higher point size before scaling it down.
|
|
16
17
|
* Pages grow on demand until `maxPages` is reached — the GPU renderer pins that at one page so a
|
|
17
18
|
* single draw call covers the whole atlas — and a `revision` counter tells the renderer which of
|
|
18
19
|
* them changed since the last upload.
|
|
19
20
|
*/
|
|
21
|
+
import { fontFaceKey } from "./text-layout.js";
|
|
20
22
|
/** Page size and inter-glyph padding, matching reference minimum atlas and `GLYPH_ATLAS_PADDING`. */
|
|
21
23
|
export const DEFAULT_ATLAS_SIZE = 512;
|
|
22
24
|
export const DEFAULT_ATLAS_PADDING = 2;
|
|
@@ -100,13 +102,14 @@ export class GlyphAtlas {
|
|
|
100
102
|
* exactly as it did. The renderer passes the size the block covers on screen instead, so the entry
|
|
101
103
|
* that comes back is rasterised for the rung that face asked for.
|
|
102
104
|
*/
|
|
103
|
-
glyph(character,
|
|
105
|
+
glyph(character, face, fontSize, rasterScale) {
|
|
104
106
|
const scale = rasterScale !== undefined && rasterScale > 0 ? rasterScale : this.rasterScale;
|
|
105
107
|
// The key carries the size the glyph was *rasterised* at rather than the layout font size: two
|
|
106
108
|
// font sizes that land on the same rung share one rasterisation, which is what keeps a block
|
|
107
109
|
// whose size changes every frame from rasterising the same character over and over.
|
|
108
110
|
const rasterPx = Math.max(1, Math.round(fontSize * scale));
|
|
109
|
-
const
|
|
111
|
+
const faceKey = this.#rasterizer.faceKey?.(face) ?? fontFaceKey(face);
|
|
112
|
+
const key = `${faceKey}|${rasterPx}|${character}`;
|
|
110
113
|
const cached = this.#glyphs.get(key);
|
|
111
114
|
if (cached !== undefined) {
|
|
112
115
|
cached.lastFrame = this.#frame;
|
|
@@ -114,7 +117,7 @@ export class GlyphAtlas {
|
|
|
114
117
|
}
|
|
115
118
|
if (this.#missing.has(key))
|
|
116
119
|
return undefined;
|
|
117
|
-
const rasterized = this.#rasterizer.rasterize(character,
|
|
120
|
+
const rasterized = this.#rasterizer.rasterize(character, face, rasterPx);
|
|
118
121
|
if (rasterized === undefined) {
|
|
119
122
|
this.#missing.add(key);
|
|
120
123
|
return undefined;
|
package/dist/text-font.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Everything here is browser-only. Layout depends on the `TextMeasurer` interface alone, so
|
|
14
14
|
* unit tests keep running in plain Node, and the GPU tests inject their own rasteriser.
|
|
15
15
|
*/
|
|
16
|
-
import type
|
|
16
|
+
import { type FontFace, type GlyphMetrics, type TextMeasurer, type VerticalMetrics } from "./text-layout.js";
|
|
17
17
|
/**
|
|
18
18
|
* How a glyph is stored in the atlas, and therefore how the shader has to decode it.
|
|
19
19
|
*
|
|
@@ -36,6 +36,23 @@ export type GlyphEncoding = "bitmap" | "sdf" | "msdf";
|
|
|
36
36
|
export declare const DEFAULT_FONT_FAMILY = "vgpu-render-fira-mono";
|
|
37
37
|
/** Decodes a base64 payload into bytes. Shared by the inlined font and the prebaked atlas. */
|
|
38
38
|
export declare function decodeBase64Bytes(base64: string): Uint8Array<ArrayBuffer>;
|
|
39
|
+
/** One inlined face of a multi-weight family: the weight it was built at, plus its WOFF2 bytes. */
|
|
40
|
+
export type EmbeddedFontFace = {
|
|
41
|
+
readonly weight: number;
|
|
42
|
+
/** Base64 WOFF2, so the payload stays plain text and every toolchain can read it. */
|
|
43
|
+
readonly woff2Base64: string;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Registers a family whose weights are separate faces, each carrying its own `weight` descriptor.
|
|
47
|
+
*
|
|
48
|
+
* This is the only way to get more than one drawn weight out of Canvas2D: the font shorthand carries a
|
|
49
|
+
* single synthesised bold step, and `font-variation-settings` does not exist there, but the browser
|
|
50
|
+
* does select a face by the weight it was registered with. Registering the same family once per
|
|
51
|
+
* weight therefore turns `weight: 700` into a real bold face instead of an emboldened regular one.
|
|
52
|
+
*
|
|
53
|
+
* Idempotent per family, like `loadDefaultFont`, and browser-only.
|
|
54
|
+
*/
|
|
55
|
+
export declare function loadFontFaces(family: string, faces: readonly EmbeddedFontFace[]): Promise<void>;
|
|
39
56
|
/** Decodes the inlined WOFF2 bytes of the bundled font, which is what `FontFace` takes. */
|
|
40
57
|
export declare function decodeEmbeddedFont(): Uint8Array<ArrayBuffer>;
|
|
41
58
|
/**
|
|
@@ -102,6 +119,15 @@ export interface GlyphRasterizer {
|
|
|
102
119
|
* were rasterised once at a fixed size, so the renderer leaves it on `rasterScale` instead.
|
|
103
120
|
*/
|
|
104
121
|
readonly dynamicRaster?: boolean;
|
|
122
|
+
/**
|
|
123
|
+
* Cache-key part for one face. The atlas keys glyphs by this plus the raster size, so a source that
|
|
124
|
+
* cannot change a face' axes reports just the parts it honours.
|
|
125
|
+
*
|
|
126
|
+
* The default keys on every axis (`fontFaceKey`). A source holding one fixed face — a prebaked page
|
|
127
|
+
* or a single parsed `opentype.js` buffer — overrides it so a weight the source cannot rasterise
|
|
128
|
+
* does not multiply identical atlas entries.
|
|
129
|
+
*/
|
|
130
|
+
readonly faceKey?: (face: FontFace) => string;
|
|
105
131
|
/**
|
|
106
132
|
* Returns `undefined` for characters with no ink, such as whitespace.
|
|
107
133
|
*
|
|
@@ -109,10 +135,25 @@ export interface GlyphRasterizer {
|
|
|
109
135
|
* size: the atlas asks for the size the glyph will cover on screen, so the same character stays
|
|
110
136
|
* sharp at any font size while still being rasterised once per size rung.
|
|
111
137
|
*/
|
|
112
|
-
rasterize(character: string,
|
|
113
|
-
/** Vertical metrics for a `fontSize`-sized
|
|
114
|
-
verticalMetrics(
|
|
138
|
+
rasterize(character: string, face: FontFace, rasterSize: number): RasterizedGlyph | undefined;
|
|
139
|
+
/** Vertical metrics for a `fontSize`-sized instance of `face`. */
|
|
140
|
+
verticalMetrics(face: FontFace, fontSize: number): VerticalMetrics;
|
|
115
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* The CSS font shorthand that selects `face` at `fontSize`.
|
|
144
|
+
*
|
|
145
|
+
* Order follows the shorthand's grammar — style, weight, stretch, size, family — because Canvas2D
|
|
146
|
+
* parses the value as one. Two of the axes are conditional:
|
|
147
|
+
*
|
|
148
|
+
* - `font-stretch` keywords are only kept by the parser when the face has a `wdth` axis. A static
|
|
149
|
+
* face drops the keyword and draws every stop identically, which is why the bundled subset cannot
|
|
150
|
+
* show a `FontWidth` difference at all.
|
|
151
|
+
* - `italic` is the only slant Canvas2D keeps; `oblique <angle>` reads back as plain `italic`.
|
|
152
|
+
*
|
|
153
|
+
* A weight the loaded face does not carry is synthesised by the browser (one emboldened step, which
|
|
154
|
+
* keeps the advance), so `weight` at least changes what is drawn for the bundled static face.
|
|
155
|
+
*/
|
|
156
|
+
export declare function canvasFontString(face: FontFace, fontSize: number): string;
|
|
116
157
|
/** Encodings Canvas2D coverage can produce directly. `msdf` needs outlines, not a mask. */
|
|
117
158
|
export type CoverageEncoding = Exclude<GlyphEncoding, "msdf">;
|
|
118
159
|
/**
|
|
@@ -141,9 +182,9 @@ export declare class CanvasGlyphRasterizer implements GlyphRasterizer, TextMeasu
|
|
|
141
182
|
rasterScale?: number;
|
|
142
183
|
dynamicRaster?: boolean;
|
|
143
184
|
});
|
|
144
|
-
verticalMetrics(
|
|
145
|
-
glyphMetrics(character: string,
|
|
146
|
-
rasterize(character: string,
|
|
185
|
+
verticalMetrics(face: FontFace, fontSize: number): VerticalMetrics;
|
|
186
|
+
glyphMetrics(character: string, face: FontFace, fontSize: number): GlyphMetrics;
|
|
187
|
+
rasterize(character: string, face: FontFace, rasterSize: number): RasterizedGlyph | undefined;
|
|
147
188
|
}
|
|
148
189
|
/** Packs Canvas2D coverage into the RGBA layout the chosen encoding expects. */
|
|
149
190
|
export declare function encodeCoverage(coverage: Uint8Array, width: number, height: number, encoding: CoverageEncoding, range: number): Uint8Array;
|
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";
|