three-text 0.6.1 → 0.6.2

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 CHANGED
@@ -19,8 +19,6 @@ High fidelity 3D text rendering and layout for the web
19
19
 
20
20
  The library has a framework-agnostic core with lightweight adapters for [Three.js](https://threejs.org), [React Three Fiber](https://docs.pmnd.rs/react-three-fiber), [p5.js](https://p5js.org), [WebGL](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API), and [WebGPU](https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API)
21
21
 
22
- Under the hood, three-text relies on a core of [harfbuzzjs](https://github.com/harfbuzz/harfbuzzjs) (based on [HarfBuzz](https://github.com/harfbuzz/harfbuzz) by Behdad Esfahbod et al) for text shaping, [Knuth-Plass](http://www.eprg.org/G53DOC/pdfs/knuth-plass-breaking.pdf) line breaking (with [SILE](https://github.com/sile-typesetter/sile/blob/master/core/break.lua) and LuaTex being the closest modern references), [Liang](https://tug.org/docs/liang/liang-thesis.pdf) hyphenation and the [TeX hyphenation patterns](https://github.com/hyphenation/tex-hyphen), and [woff-lib](https://github.com/countertype/woff-lib) for optional WOFF2 support. The mesh text pipeline uses [libtess-ts](https://github.com/countertype/libtess-ts) (a port of the [GLU tessellator](https://www.songho.ca/opengl/gl_tessellation.html) by Eric Veach) for removing overlaps and triangulation, adaptive curve polygonization from Maxim Shemanarev's [Anti-Grain Geometry](https://web.archive.org/web/20060128212843/http://www.antigrain.com/research/adaptive_bezier/index.html), and [Visvalingam-Whyatt](https://hull-repository.worktribe.com/preview/376364/000870493786962263.pdf) [line simplification](https://bost.ocks.org/mike/simplify/). The vector pipeline uses [Slug](https://github.com/EricLengyel/Slug) by Eric Lengyel for resolution-independent curve rendering
23
-
24
22
  ## Table of contents
25
23
 
26
24
  - [Overview](#overview)
@@ -41,6 +39,7 @@ Under the hood, three-text relies on a core of [harfbuzzjs](https://github.com/h
41
39
  - [Browser compatibility](#browser-compatibility)
42
40
  - [Testing](#testing)
43
41
  - [Build system](#build-system)
42
+ - [Entry points](#entry-points)
44
43
  - [Build outputs](#build-outputs)
45
44
  - [Acknowledgements](#acknowledgements)
46
45
  - [License](#license)
@@ -60,34 +59,14 @@ npm install three
60
59
 
61
60
  ## Architecture
62
61
 
63
- three-text has a framework-agnostic core that processes fonts and generates geometry data. Lightweight adapters convert this data to framework-specific formats:
64
-
65
- - **`three-text`** - Three.js adapter (default export, returns `BufferGeometry`)
66
- - **`three-text/mesh`** - Same as above (explicit alias)
67
- - **`three-text/mesh/react`** - React Three Fiber component for extruded mesh text
68
- - **`three-text/three`** - Deprecated, use `three-text/mesh`
69
- - **`three-text/three/react`** - Deprecated, use `three-text/mesh/react`
70
- - **`three-text/mesh/webgl`** - WebGL mesh buffer utility
71
- - **`three-text/mesh/webgpu`** - WebGPU mesh buffer utility
72
- - **`three-text/mesh/p5`** - p5.js adapter
73
- - **`three-text/core`** - Framework-agnostic core (returns raw arrays)
74
- - **`three-text/vector`** - Vector rendering (Slug per-fragment curve evaluation), `Text.create()` returns a `THREE.Group`
75
- - **`three-text/vector/react`** - React Three Fiber component for vector text
76
- - **`three-text/vector/core`** - Framework-agnostic vector core (returns raw `SlugGPUData`, no Three.js dependency)
77
- - **`three-text/vector/webgl`** - Raw WebGL2 vector renderer (no Three.js dependency)
78
- - **`three-text/vector/webgpu`** - Raw WebGPU vector renderer (no Three.js dependency)
79
- - **`three-text/webgl`** - Deprecated, use `three-text/mesh/webgl`
80
- - **`three-text/webgpu`** - Deprecated, use `three-text/mesh/webgpu`
81
- - **`three-text/p5`** - Deprecated, use `three-text/mesh/p5`
82
-
83
- Most users will just `import { Text } from 'three-text'` for Three.js projects with mesh, or `import { Text } from 'three-text/vector'` for vector text
62
+ three-text has a framework-agnostic core that processes fonts and generates geometry data. Lightweight adapters convert this data to framework-specific formats. Most users will just `import { Text } from 'three-text'` for Three.js projects with mesh, or `import { Text } from 'three-text/vector'` for vector text. See [Entry points](#entry-points) for the full list of adapters
84
63
 
85
64
  ### Mesh vs vector
86
65
 
87
66
  The library offers two rendering modes that share the same core (HarfBuzz shaping, Knuth-Plass justification, glyph caching):
88
67
 
89
68
  - **Mesh** (`three-text` (default) / `three-text/mesh`): triangulated geometry you can extrude, light, and shade. Use for 3D text, text in a scene graph, or anywhere you need depth
90
- - **Vector** (`three-text/vector`): resolution-independent rendering on the GPU via per-fragment curve evaluation. No tessellation, no stencil buffer. Use for text that needs to stay sharp at arbitrary zoom
69
+ - **Vector** (`three-text/vector`): resolution-independent rendering on the GPU via per-fragment curve evaluation. Use for flat text that needs to stay sharp at arbitrary zoom
91
70
 
92
71
  Both can be used in the same project from separate entry points
93
72
 
@@ -483,6 +462,8 @@ Existing solutions take different approaches:
483
462
 
484
463
  three-text produces actual geometry from font files, sharper at close distances than bitmap approaches, with control over typesetting and paragraph justification via TeX-based parameters
485
464
 
465
+ Under the hood, three-text relies on a core of [harfbuzzjs](https://github.com/harfbuzz/harfbuzzjs) (based on [HarfBuzz](https://github.com/harfbuzz/harfbuzz) by Behdad Esfahbod et al) for text shaping, [Knuth-Plass](http://www.eprg.org/G53DOC/pdfs/knuth-plass-breaking.pdf) line breaking (with [SILE](https://github.com/sile-typesetter/sile/blob/master/core/break.lua) and LuaTex being the closest modern references), [Liang](https://tug.org/docs/liang/liang-thesis.pdf) hyphenation and the [TeX hyphenation patterns](https://github.com/hyphenation/tex-hyphen), and [woff-lib](https://github.com/countertype/woff-lib) for optional WOFF2 support. The mesh text pipeline uses [libtess-ts](https://github.com/countertype/libtess-ts) (a port of the [GLU tessellator](https://www.songho.ca/opengl/gl_tessellation.html) by Eric Veach) for removing overlaps and triangulation, adaptive curve polygonization from Maxim Shemanarev's [Anti-Grain Geometry](https://web.archive.org/web/20060128212843/http://www.antigrain.com/research/adaptive_bezier/index.html), and [Visvalingam-Whyatt](https://hull-repository.worktribe.com/preview/376364/000870493786962263.pdf) [line simplification](https://bost.ocks.org/mike/simplify/). The vector pipeline uses [Slug](https://github.com/EricLengyel/Slug) by Eric Lengyel for resolution-independent curve rendering
466
+
486
467
  ### Why Slug
487
468
 
488
469
  The vector path uses the Slug algorithm by Eric Lengyel. Each glyph is a single quad; the fragment shader evaluates curve coverage analytically to compute a winding number. Because it operates on winding rather than geometry, it naturally handles the self-intersecting contours that variable fonts produce when axes like weight push outlines into each other
@@ -1256,6 +1237,22 @@ git submodule update --init --recursive
1256
1237
 
1257
1238
  The script then processes the TeX hyphenation data into optimized trie structures. The process is slow for the complete set of languages (~1 minute on an M2 Max), so using `--languages` for development is recommended
1258
1239
 
1240
+ ## Entry points
1241
+
1242
+ - **`three-text`** - Three.js adapter (default export, returns mesh `BufferGeometry`)
1243
+ - **`three-text/mesh`** - Same as above (explicit alias)
1244
+ - **`three-text/mesh/react`** - React Three Fiber component for extruded mesh text
1245
+ - **`three-text/three/react`** - Deprecated, use `three-text/mesh/react`
1246
+ - **`three-text/mesh/webgl`** - WebGL mesh buffer utility
1247
+ - **`three-text/mesh/webgpu`** - WebGPU mesh buffer utility
1248
+ - **`three-text/mesh/p5`** - p5.js adapter
1249
+ - **`three-text/core`** - Framework-agnostic core (returns raw arrays)
1250
+ - **`three-text/vector`** - Vector rendering (Slug per-fragment curve evaluation), `Text.create()` returns a `THREE.Group`
1251
+ - **`three-text/vector/react`** - React Three Fiber component for vector text
1252
+ - **`three-text/vector/core`** - Framework-agnostic vector core (returns raw `SlugGPUData`, no Three.js dependency)
1253
+ - **`three-text/vector/webgl`** - Raw WebGL2 vector renderer (no Three.js dependency)
1254
+ - **`three-text/vector/webgpu`** - Raw WebGPU vector renderer (no Three.js dependency)
1255
+
1259
1256
  ## Build outputs
1260
1257
 
1261
1258
  The build generates multiple module formats for core and all adapters:
@@ -1289,4 +1286,4 @@ The build generates multiple module formats for core and all adapters:
1289
1286
 
1290
1287
  `three-text` was written by Jeremy Tribby ([@jpt](https://github.com/jpt)) and is licensed under the MIT License. See the [LICENSE](LICENSE) file for details
1291
1288
 
1292
- This software includes code from third-party libraries under compatible permissive licenses. For full license details, see the [LICENSE_THIRD_PARTY](LICENSE_THIRD_PARTY) file
1289
+ This software includes code from third-party libraries under compatible permissive licenses. For full license details, see the [LICENSE_THIRD_PARTY](LICENSE_THIRD_PARTY) file
package/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * @license
3
- * three-text v0.6.1
3
+ * three-text v0.6.2
4
4
  * Copyright © 2025-2026 Jeremy Tribby, Countertype LLC
5
5
  * SPDX-License-Identifier: MIT
6
6
  */
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * @license
3
- * three-text v0.6.1
3
+ * three-text v0.6.2
4
4
  * Copyright © 2025-2026 Jeremy Tribby, Countertype LLC
5
5
  * SPDX-License-Identifier: MIT
6
6
  */
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * @license
3
- * three-text v0.6.1
3
+ * three-text v0.6.2
4
4
  * Copyright © 2025-2026 Jeremy Tribby, Countertype LLC
5
5
  * SPDX-License-Identifier: MIT
6
6
  */
package/dist/index.min.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * @license
3
- * three-text v0.6.1
3
+ * three-text v0.6.2
4
4
  * Copyright © 2025-2026 Jeremy Tribby, Countertype LLC
5
5
  * SPDX-License-Identifier: MIT
6
6
  */
package/dist/index.umd.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * @license
3
- * three-text v0.6.1
3
+ * three-text v0.6.2
4
4
  * Copyright © 2025-2026 Jeremy Tribby, Countertype LLC
5
5
  * SPDX-License-Identifier: MIT
6
6
  */
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * @license
3
- * three-text v0.6.1
3
+ * three-text v0.6.2
4
4
  * Copyright © 2025-2026 Jeremy Tribby, Countertype LLC
5
5
  * SPDX-License-Identifier: MIT
6
6
  */
@@ -258,7 +258,6 @@ class GlyphOutlineCollector {
258
258
  // GPU-ready packed textures + vertex attribute buffers
259
259
  const TEX_WIDTH = 4096;
260
260
  const LOG_TEX_WIDTH = 12;
261
- // Float/Uint32 reinterpretation helpers
262
261
  const _f32 = new Float32Array(1);
263
262
  const _u32 = new Uint32Array(_f32.buffer);
264
263
  function uintAsFloat(u) {
@@ -270,9 +269,7 @@ function uintAsFloat(u) {
270
269
  function packSlugData(shapes, options) {
271
270
  const bandCount = options?.bandCount ?? 16;
272
271
  const evenOdd = options?.evenOdd ?? false;
273
- // Pack all curves into curveTexture
274
272
  const allCurves = [];
275
- // Estimate max texels needed
276
273
  let totalCurves = 0;
277
274
  for (const shape of shapes) {
278
275
  totalCurves += shape.curves.length;
@@ -285,13 +282,12 @@ function packSlugData(shapes, options) {
285
282
  const entries = [];
286
283
  const [ox, oy] = shape.bounds;
287
284
  for (const curve of shape.curves) {
288
- // Don't let a curve span across row boundary (needs 2 consecutive texels)
289
285
  if (curveX >= TEX_WIDTH - 1) {
290
286
  curveX = 0;
291
287
  curveY++;
292
288
  }
293
- // Store control points in glyph-local space (relative to bounds min)
294
- // so the fragment shader's renderCoord subtraction stays near zero
289
+ // Glyph-local space (relative to bounds min) keeps renderCoord
290
+ // subtraction near zero, preserving float32 precision in the solver
295
291
  const lp1x = curve.p1[0] - ox, lp1y = curve.p1[1] - oy;
296
292
  const lp2x = curve.p2[0] - ox, lp2y = curve.p2[1] - oy;
297
293
  const lp3x = curve.p3[0] - ox, lp3y = curve.p3[1] - oy;
@@ -320,12 +316,10 @@ function packSlugData(shapes, options) {
320
316
  allCurves.push(entries);
321
317
  }
322
318
  const actualCurveTexHeight = curveY + 1;
323
- // Build band data for each shape and pack into bandTexture
324
- // Layout per shape in bandTexture (relative to glyphLoc):
325
- // [0 .. hBandMax] : h-band headers
326
- // [hBandMax+1 .. hBandMax+1+vBandMax] : v-band headers
327
- // [hBandMax+vBandMax+2 .. ] : curve index lists
328
- // First pass: compute total band texels needed
319
+ // Band texture layout per shape (relative to glyphLoc):
320
+ // [0 .. hBandMax] h-band headers
321
+ // [hBandMax+1 .. hBandMax+1+vBandMax] v-band headers
322
+ // [hBandMax+vBandMax+2 .. ] curve index lists
329
323
  const shapeBandData = [];
330
324
  let totalBandTexels = 0;
331
325
  for (let si = 0; si < shapes.length; si++) {
@@ -345,21 +339,19 @@ function packSlugData(shapes, options) {
345
339
  const vBandCount = Math.min(bandCount, 255);
346
340
  const bandMaxY = hBandCount - 1;
347
341
  const bandMaxX = vBandCount - 1;
348
- // Build horizontal bands (partition y-axis, glyph-local coords)
342
+ // Horizontal bands (partition y-axis)
349
343
  const hBands = [];
350
344
  const hLists = [];
351
345
  const bandH = h / hBandCount;
352
346
  for (let bi = 0; bi < hBandCount; bi++) {
353
347
  const bandMinY = bi * bandH;
354
348
  const bandMaxYCoord = bandMinY + bandH;
355
- // Collect curves whose y-range overlaps this band
356
349
  const list = [];
357
350
  for (const c of curves) {
358
351
  if (c.maxY >= bandMinY && c.minY <= bandMaxYCoord) {
359
352
  list.push({ curve: c, sortKey: c.maxX });
360
353
  }
361
354
  }
362
- // Sort by descending max-x for early exit
363
355
  list.sort((a, b) => b.sortKey - a.sortKey);
364
356
  const flatList = [];
365
357
  for (const item of list) {
@@ -368,7 +360,7 @@ function packSlugData(shapes, options) {
368
360
  hBands.push({ curveCount: list.length, listOffset: 0 });
369
361
  hLists.push(flatList);
370
362
  }
371
- // Build vertical bands (partition x-axis, glyph-local coords)
363
+ // Vertical bands (partition x-axis)
372
364
  const vBands = [];
373
365
  const vLists = [];
374
366
  const bandW = w / vBandCount;
@@ -381,7 +373,6 @@ function packSlugData(shapes, options) {
381
373
  list.push({ curve: c, sortKey: c.maxY });
382
374
  }
383
375
  }
384
- // Sort by descending max-y for early exit
385
376
  list.sort((a, b) => b.sortKey - a.sortKey);
386
377
  const flatList = [];
387
378
  for (const item of list) {
@@ -390,7 +381,6 @@ function packSlugData(shapes, options) {
390
381
  vBands.push({ curveCount: list.length, listOffset: 0 });
391
382
  vLists.push(flatList);
392
383
  }
393
- // Total texels for this shape: band headers + curve lists
394
384
  const headerTexels = hBandCount + vBandCount;
395
385
  let listTexels = 0;
396
386
  for (const l of hLists)
@@ -404,10 +394,8 @@ function packSlugData(shapes, options) {
404
394
  });
405
395
  totalBandTexels += total;
406
396
  }
407
- // Allocate bandTexture (extra rows for row-alignment padding of curve lists)
408
397
  const bandTexHeight = Math.max(1, Math.ceil(totalBandTexels / TEX_WIDTH) + shapes.length * 2);
409
398
  const bandData = new Uint32Array(TEX_WIDTH * bandTexHeight * 4);
410
- // Pack band data per shape
411
399
  let bandX = 0;
412
400
  let bandY = 0;
413
401
  const glyphLocs = [];
@@ -417,11 +405,8 @@ function packSlugData(shapes, options) {
417
405
  glyphLocs.push({ x: 0, y: 0 });
418
406
  continue;
419
407
  }
420
- // Ensure glyph data doesn't start too close to row end
421
- // (need at least headerTexels contiguous... actually wrapping is handled by CalcBandLoc)
422
- // But the initial band header reads don't use CalcBandLoc, so glyphLoc.x + bandMax.y + 1 + bandMaxX
423
- // must be reachable. CalcBandLoc handles wrapping for curve lists.
424
- // To be safe, start each glyph at the beginning of a row if remaining space is tight.
408
+ // Band headers are read without CalcBandLoc wrapping, so all
409
+ // headers for a glyph must fit within a single texture row
425
410
  const minContiguous = sd.hBands.length + sd.vBands.length;
426
411
  if (bandX + minContiguous > TEX_WIDTH) {
427
412
  bandX = 0;
@@ -430,11 +415,8 @@ function packSlugData(shapes, options) {
430
415
  const glyphLocX = bandX;
431
416
  const glyphLocY = bandY;
432
417
  glyphLocs.push({ x: glyphLocX, y: glyphLocY });
433
- // Curve lists start after all headers
434
418
  let listStartOffset = sd.hBands.length + sd.vBands.length;
435
- // The shader reads curve list entries at (hbandLoc.x + curveIndex, hbandLoc.y)
436
- // with NO row wrapping. Each list must fit entirely within a single texture row.
437
- // Pad the offset to the next row start when a list would cross a row boundary.
419
+ // Curve lists aren't row-wrapped by the shader, so pad to avoid crossing
438
420
  const ensureListFits = (listLen) => {
439
421
  if (listLen === 0)
440
422
  return;
@@ -443,21 +425,18 @@ function packSlugData(shapes, options) {
443
425
  listStartOffset += (TEX_WIDTH - startX);
444
426
  }
445
427
  };
446
- // Assign list offsets for h-bands
447
428
  for (let bi = 0; bi < sd.hBands.length; bi++) {
448
429
  const listLen = sd.hLists[bi].length / 2;
449
430
  ensureListFits(listLen);
450
431
  sd.hBands[bi].listOffset = listStartOffset;
451
432
  listStartOffset += listLen;
452
433
  }
453
- // Assign list offsets for v-bands
454
434
  for (let bi = 0; bi < sd.vBands.length; bi++) {
455
435
  const listLen = sd.vLists[bi].length / 2;
456
436
  ensureListFits(listLen);
457
437
  sd.vBands[bi].listOffset = listStartOffset;
458
438
  listStartOffset += listLen;
459
439
  }
460
- // Write h-band headers
461
440
  for (let bi = 0; bi < sd.hBands.length; bi++) {
462
441
  const tx = glyphLocX + bi;
463
442
  const ty = glyphLocY;
@@ -467,7 +446,6 @@ function packSlugData(shapes, options) {
467
446
  bandData[idx + 2] = 0;
468
447
  bandData[idx + 3] = 0;
469
448
  }
470
- // Write v-band headers (after h-bands)
471
449
  const vBandStart = glyphLocX + sd.hBands.length;
472
450
  for (let bi = 0; bi < sd.vBands.length; bi++) {
473
451
  const tx = vBandStart + bi;
@@ -478,9 +456,7 @@ function packSlugData(shapes, options) {
478
456
  bandData[idx + 2] = 0;
479
457
  bandData[idx + 3] = 0;
480
458
  }
481
- // Write curve lists using CalcBandLoc-style wrapping
482
459
  const texWidthMask = (1 << LOG_TEX_WIDTH) - 1;
483
- // Write h-band curve lists
484
460
  for (let bi = 0; bi < sd.hBands.length; bi++) {
485
461
  const list = sd.hLists[bi];
486
462
  const baseOffset = sd.hBands[bi].listOffset;
@@ -489,13 +465,12 @@ function packSlugData(shapes, options) {
489
465
  const by = glyphLocY + (bx >> LOG_TEX_WIDTH);
490
466
  bx &= texWidthMask;
491
467
  const idx = (by * TEX_WIDTH + bx) * 4;
492
- bandData[idx + 0] = list[ci]; // curveTexX
493
- bandData[idx + 1] = list[ci + 1]; // curveTexY
468
+ bandData[idx + 0] = list[ci];
469
+ bandData[idx + 1] = list[ci + 1];
494
470
  bandData[idx + 2] = 0;
495
471
  bandData[idx + 3] = 0;
496
472
  }
497
473
  }
498
- // Write v-band curve lists
499
474
  for (let bi = 0; bi < sd.vBands.length; bi++) {
500
475
  const list = sd.vLists[bi];
501
476
  const baseOffset = sd.vBands[bi].listOffset;
@@ -510,24 +485,20 @@ function packSlugData(shapes, options) {
510
485
  bandData[idx + 3] = 0;
511
486
  }
512
487
  }
513
- // Advance band cursor past this shape's data
514
488
  let endBx = glyphLocX + listStartOffset;
515
489
  bandY = glyphLocY + (endBx >> LOG_TEX_WIDTH);
516
490
  bandX = endBx & texWidthMask;
517
491
  }
518
492
  const actualBandTexHeight = bandY + 1;
519
- // Build vertex attributes
520
- // 5 attribs x 4 floats x 4 vertices per shape = 80 floats per shape
521
493
  const FLOATS_PER_VERTEX = 20; // 5 attribs * 4 components
522
494
  const VERTS_PER_SHAPE = 4;
523
495
  const vertices = new Float32Array(shapes.length * VERTS_PER_SHAPE * FLOATS_PER_VERTEX);
524
496
  const indices = new Uint16Array(shapes.length * 6);
525
- // Corner normals (outward-pointing, un-normalized; SlugDilate normalizes)
526
497
  const cornerNormals = [
527
- [-1, -1], // bottom-left
528
- [1, -1], // bottom-right
529
- [1, 1], // top-right
530
- [-1, 1], // top-left
498
+ [-1, -1],
499
+ [1, -1],
500
+ [1, 1],
501
+ [-1, 1],
531
502
  ];
532
503
  for (let si = 0; si < shapes.length; si++) {
533
504
  const shape = shapes[si];
@@ -536,28 +507,23 @@ function packSlugData(shapes, options) {
536
507
  const [bMinX, bMinY, bMaxX, bMaxY] = shape.bounds;
537
508
  const w = bMaxX - bMinX;
538
509
  const h = bMaxY - bMinY;
539
- // Corner positions in object-space
540
510
  const corners = [
541
511
  [bMinX, bMinY],
542
512
  [bMaxX, bMinY],
543
513
  [bMaxX, bMaxY],
544
514
  [bMinX, bMaxY],
545
515
  ];
546
- // Em-space sample coords in glyph-local space (origin at bounds min)
547
516
  const emCorners = [
548
517
  [0, 0],
549
518
  [w, 0],
550
519
  [w, h],
551
520
  [0, h],
552
521
  ];
553
- // Pack tex.z: glyph location in band texture
554
522
  const texZ = uintAsFloat((glyph.x & 0xFFFF) | ((glyph.y & 0xFFFF) << 16));
555
- // Pack tex.w: band max + flags
556
523
  let texWBits = (sd.bandMaxX & 0xFF) | ((sd.bandMaxY & 0xFF) << 16);
557
524
  if (evenOdd)
558
525
  texWBits |= 0x10000000; // E flag at bit 28
559
526
  const texW = uintAsFloat(texWBits);
560
- // Band transform: scale to map glyph-local em-coords to band indices
561
527
  const bandScaleX = w > 0 ? sd.vBands.length / w : 0;
562
528
  const bandScaleY = h > 0 ? sd.hBands.length / h : 0;
563
529
  for (let vi = 0; vi < 4; vi++) {
@@ -572,23 +538,22 @@ function packSlugData(shapes, options) {
572
538
  vertices[base + 5] = emCorners[vi][1];
573
539
  vertices[base + 6] = texZ;
574
540
  vertices[base + 7] = texW;
575
- // jac: identity Jacobian (em-space is a pure translation of object-space)
541
+ // jac: identity (em-space is a translation of object-space)
576
542
  vertices[base + 8] = 1.0;
577
543
  vertices[base + 9] = 0.0;
578
544
  vertices[base + 10] = 0.0;
579
545
  vertices[base + 11] = 1.0;
580
- // bnd: band scale (offset is zero in glyph-local space)
546
+ // bnd: band scale (offset zero in glyph-local space)
581
547
  vertices[base + 12] = bandScaleX;
582
548
  vertices[base + 13] = bandScaleY;
583
549
  vertices[base + 14] = 0;
584
550
  vertices[base + 15] = 0;
585
- // col: white with full alpha (caller overrides via uniform or attribute)
551
+ // col
586
552
  vertices[base + 16] = 1.0;
587
553
  vertices[base + 17] = 1.0;
588
554
  vertices[base + 18] = 1.0;
589
555
  vertices[base + 19] = 1.0;
590
556
  }
591
- // Indices: two triangles per quad
592
557
  const vBase = si * 4;
593
558
  const iBase = si * 6;
594
559
  indices[iBase + 0] = vBase + 0;
@@ -797,7 +762,6 @@ function computePlaneBounds(glyphInfos) {
797
762
  }
798
763
  return { min: { x: minX, y: minY, z: 0 }, max: { x: maxX, y: maxY, z: 0 } };
799
764
  }
800
- // Public API
801
765
  function buildVectorResult(layoutHandle, ctx, options) {
802
766
  const scale = layoutHandle.layoutData.pixelsPerFontUnit;
803
767
  let cachedQuery = null;
@@ -256,7 +256,6 @@ class GlyphOutlineCollector {
256
256
  // GPU-ready packed textures + vertex attribute buffers
257
257
  const TEX_WIDTH = 4096;
258
258
  const LOG_TEX_WIDTH = 12;
259
- // Float/Uint32 reinterpretation helpers
260
259
  const _f32 = new Float32Array(1);
261
260
  const _u32 = new Uint32Array(_f32.buffer);
262
261
  function uintAsFloat(u) {
@@ -268,9 +267,7 @@ function uintAsFloat(u) {
268
267
  function packSlugData(shapes, options) {
269
268
  const bandCount = options?.bandCount ?? 16;
270
269
  const evenOdd = options?.evenOdd ?? false;
271
- // Pack all curves into curveTexture
272
270
  const allCurves = [];
273
- // Estimate max texels needed
274
271
  let totalCurves = 0;
275
272
  for (const shape of shapes) {
276
273
  totalCurves += shape.curves.length;
@@ -283,13 +280,12 @@ function packSlugData(shapes, options) {
283
280
  const entries = [];
284
281
  const [ox, oy] = shape.bounds;
285
282
  for (const curve of shape.curves) {
286
- // Don't let a curve span across row boundary (needs 2 consecutive texels)
287
283
  if (curveX >= TEX_WIDTH - 1) {
288
284
  curveX = 0;
289
285
  curveY++;
290
286
  }
291
- // Store control points in glyph-local space (relative to bounds min)
292
- // so the fragment shader's renderCoord subtraction stays near zero
287
+ // Glyph-local space (relative to bounds min) keeps renderCoord
288
+ // subtraction near zero, preserving float32 precision in the solver
293
289
  const lp1x = curve.p1[0] - ox, lp1y = curve.p1[1] - oy;
294
290
  const lp2x = curve.p2[0] - ox, lp2y = curve.p2[1] - oy;
295
291
  const lp3x = curve.p3[0] - ox, lp3y = curve.p3[1] - oy;
@@ -318,12 +314,10 @@ function packSlugData(shapes, options) {
318
314
  allCurves.push(entries);
319
315
  }
320
316
  const actualCurveTexHeight = curveY + 1;
321
- // Build band data for each shape and pack into bandTexture
322
- // Layout per shape in bandTexture (relative to glyphLoc):
323
- // [0 .. hBandMax] : h-band headers
324
- // [hBandMax+1 .. hBandMax+1+vBandMax] : v-band headers
325
- // [hBandMax+vBandMax+2 .. ] : curve index lists
326
- // First pass: compute total band texels needed
317
+ // Band texture layout per shape (relative to glyphLoc):
318
+ // [0 .. hBandMax] h-band headers
319
+ // [hBandMax+1 .. hBandMax+1+vBandMax] v-band headers
320
+ // [hBandMax+vBandMax+2 .. ] curve index lists
327
321
  const shapeBandData = [];
328
322
  let totalBandTexels = 0;
329
323
  for (let si = 0; si < shapes.length; si++) {
@@ -343,21 +337,19 @@ function packSlugData(shapes, options) {
343
337
  const vBandCount = Math.min(bandCount, 255);
344
338
  const bandMaxY = hBandCount - 1;
345
339
  const bandMaxX = vBandCount - 1;
346
- // Build horizontal bands (partition y-axis, glyph-local coords)
340
+ // Horizontal bands (partition y-axis)
347
341
  const hBands = [];
348
342
  const hLists = [];
349
343
  const bandH = h / hBandCount;
350
344
  for (let bi = 0; bi < hBandCount; bi++) {
351
345
  const bandMinY = bi * bandH;
352
346
  const bandMaxYCoord = bandMinY + bandH;
353
- // Collect curves whose y-range overlaps this band
354
347
  const list = [];
355
348
  for (const c of curves) {
356
349
  if (c.maxY >= bandMinY && c.minY <= bandMaxYCoord) {
357
350
  list.push({ curve: c, sortKey: c.maxX });
358
351
  }
359
352
  }
360
- // Sort by descending max-x for early exit
361
353
  list.sort((a, b) => b.sortKey - a.sortKey);
362
354
  const flatList = [];
363
355
  for (const item of list) {
@@ -366,7 +358,7 @@ function packSlugData(shapes, options) {
366
358
  hBands.push({ curveCount: list.length, listOffset: 0 });
367
359
  hLists.push(flatList);
368
360
  }
369
- // Build vertical bands (partition x-axis, glyph-local coords)
361
+ // Vertical bands (partition x-axis)
370
362
  const vBands = [];
371
363
  const vLists = [];
372
364
  const bandW = w / vBandCount;
@@ -379,7 +371,6 @@ function packSlugData(shapes, options) {
379
371
  list.push({ curve: c, sortKey: c.maxY });
380
372
  }
381
373
  }
382
- // Sort by descending max-y for early exit
383
374
  list.sort((a, b) => b.sortKey - a.sortKey);
384
375
  const flatList = [];
385
376
  for (const item of list) {
@@ -388,7 +379,6 @@ function packSlugData(shapes, options) {
388
379
  vBands.push({ curveCount: list.length, listOffset: 0 });
389
380
  vLists.push(flatList);
390
381
  }
391
- // Total texels for this shape: band headers + curve lists
392
382
  const headerTexels = hBandCount + vBandCount;
393
383
  let listTexels = 0;
394
384
  for (const l of hLists)
@@ -402,10 +392,8 @@ function packSlugData(shapes, options) {
402
392
  });
403
393
  totalBandTexels += total;
404
394
  }
405
- // Allocate bandTexture (extra rows for row-alignment padding of curve lists)
406
395
  const bandTexHeight = Math.max(1, Math.ceil(totalBandTexels / TEX_WIDTH) + shapes.length * 2);
407
396
  const bandData = new Uint32Array(TEX_WIDTH * bandTexHeight * 4);
408
- // Pack band data per shape
409
397
  let bandX = 0;
410
398
  let bandY = 0;
411
399
  const glyphLocs = [];
@@ -415,11 +403,8 @@ function packSlugData(shapes, options) {
415
403
  glyphLocs.push({ x: 0, y: 0 });
416
404
  continue;
417
405
  }
418
- // Ensure glyph data doesn't start too close to row end
419
- // (need at least headerTexels contiguous... actually wrapping is handled by CalcBandLoc)
420
- // But the initial band header reads don't use CalcBandLoc, so glyphLoc.x + bandMax.y + 1 + bandMaxX
421
- // must be reachable. CalcBandLoc handles wrapping for curve lists.
422
- // To be safe, start each glyph at the beginning of a row if remaining space is tight.
406
+ // Band headers are read without CalcBandLoc wrapping, so all
407
+ // headers for a glyph must fit within a single texture row
423
408
  const minContiguous = sd.hBands.length + sd.vBands.length;
424
409
  if (bandX + minContiguous > TEX_WIDTH) {
425
410
  bandX = 0;
@@ -428,11 +413,8 @@ function packSlugData(shapes, options) {
428
413
  const glyphLocX = bandX;
429
414
  const glyphLocY = bandY;
430
415
  glyphLocs.push({ x: glyphLocX, y: glyphLocY });
431
- // Curve lists start after all headers
432
416
  let listStartOffset = sd.hBands.length + sd.vBands.length;
433
- // The shader reads curve list entries at (hbandLoc.x + curveIndex, hbandLoc.y)
434
- // with NO row wrapping. Each list must fit entirely within a single texture row.
435
- // Pad the offset to the next row start when a list would cross a row boundary.
417
+ // Curve lists aren't row-wrapped by the shader, so pad to avoid crossing
436
418
  const ensureListFits = (listLen) => {
437
419
  if (listLen === 0)
438
420
  return;
@@ -441,21 +423,18 @@ function packSlugData(shapes, options) {
441
423
  listStartOffset += (TEX_WIDTH - startX);
442
424
  }
443
425
  };
444
- // Assign list offsets for h-bands
445
426
  for (let bi = 0; bi < sd.hBands.length; bi++) {
446
427
  const listLen = sd.hLists[bi].length / 2;
447
428
  ensureListFits(listLen);
448
429
  sd.hBands[bi].listOffset = listStartOffset;
449
430
  listStartOffset += listLen;
450
431
  }
451
- // Assign list offsets for v-bands
452
432
  for (let bi = 0; bi < sd.vBands.length; bi++) {
453
433
  const listLen = sd.vLists[bi].length / 2;
454
434
  ensureListFits(listLen);
455
435
  sd.vBands[bi].listOffset = listStartOffset;
456
436
  listStartOffset += listLen;
457
437
  }
458
- // Write h-band headers
459
438
  for (let bi = 0; bi < sd.hBands.length; bi++) {
460
439
  const tx = glyphLocX + bi;
461
440
  const ty = glyphLocY;
@@ -465,7 +444,6 @@ function packSlugData(shapes, options) {
465
444
  bandData[idx + 2] = 0;
466
445
  bandData[idx + 3] = 0;
467
446
  }
468
- // Write v-band headers (after h-bands)
469
447
  const vBandStart = glyphLocX + sd.hBands.length;
470
448
  for (let bi = 0; bi < sd.vBands.length; bi++) {
471
449
  const tx = vBandStart + bi;
@@ -476,9 +454,7 @@ function packSlugData(shapes, options) {
476
454
  bandData[idx + 2] = 0;
477
455
  bandData[idx + 3] = 0;
478
456
  }
479
- // Write curve lists using CalcBandLoc-style wrapping
480
457
  const texWidthMask = (1 << LOG_TEX_WIDTH) - 1;
481
- // Write h-band curve lists
482
458
  for (let bi = 0; bi < sd.hBands.length; bi++) {
483
459
  const list = sd.hLists[bi];
484
460
  const baseOffset = sd.hBands[bi].listOffset;
@@ -487,13 +463,12 @@ function packSlugData(shapes, options) {
487
463
  const by = glyphLocY + (bx >> LOG_TEX_WIDTH);
488
464
  bx &= texWidthMask;
489
465
  const idx = (by * TEX_WIDTH + bx) * 4;
490
- bandData[idx + 0] = list[ci]; // curveTexX
491
- bandData[idx + 1] = list[ci + 1]; // curveTexY
466
+ bandData[idx + 0] = list[ci];
467
+ bandData[idx + 1] = list[ci + 1];
492
468
  bandData[idx + 2] = 0;
493
469
  bandData[idx + 3] = 0;
494
470
  }
495
471
  }
496
- // Write v-band curve lists
497
472
  for (let bi = 0; bi < sd.vBands.length; bi++) {
498
473
  const list = sd.vLists[bi];
499
474
  const baseOffset = sd.vBands[bi].listOffset;
@@ -508,24 +483,20 @@ function packSlugData(shapes, options) {
508
483
  bandData[idx + 3] = 0;
509
484
  }
510
485
  }
511
- // Advance band cursor past this shape's data
512
486
  let endBx = glyphLocX + listStartOffset;
513
487
  bandY = glyphLocY + (endBx >> LOG_TEX_WIDTH);
514
488
  bandX = endBx & texWidthMask;
515
489
  }
516
490
  const actualBandTexHeight = bandY + 1;
517
- // Build vertex attributes
518
- // 5 attribs x 4 floats x 4 vertices per shape = 80 floats per shape
519
491
  const FLOATS_PER_VERTEX = 20; // 5 attribs * 4 components
520
492
  const VERTS_PER_SHAPE = 4;
521
493
  const vertices = new Float32Array(shapes.length * VERTS_PER_SHAPE * FLOATS_PER_VERTEX);
522
494
  const indices = new Uint16Array(shapes.length * 6);
523
- // Corner normals (outward-pointing, un-normalized; SlugDilate normalizes)
524
495
  const cornerNormals = [
525
- [-1, -1], // bottom-left
526
- [1, -1], // bottom-right
527
- [1, 1], // top-right
528
- [-1, 1], // top-left
496
+ [-1, -1],
497
+ [1, -1],
498
+ [1, 1],
499
+ [-1, 1],
529
500
  ];
530
501
  for (let si = 0; si < shapes.length; si++) {
531
502
  const shape = shapes[si];
@@ -534,28 +505,23 @@ function packSlugData(shapes, options) {
534
505
  const [bMinX, bMinY, bMaxX, bMaxY] = shape.bounds;
535
506
  const w = bMaxX - bMinX;
536
507
  const h = bMaxY - bMinY;
537
- // Corner positions in object-space
538
508
  const corners = [
539
509
  [bMinX, bMinY],
540
510
  [bMaxX, bMinY],
541
511
  [bMaxX, bMaxY],
542
512
  [bMinX, bMaxY],
543
513
  ];
544
- // Em-space sample coords in glyph-local space (origin at bounds min)
545
514
  const emCorners = [
546
515
  [0, 0],
547
516
  [w, 0],
548
517
  [w, h],
549
518
  [0, h],
550
519
  ];
551
- // Pack tex.z: glyph location in band texture
552
520
  const texZ = uintAsFloat((glyph.x & 0xFFFF) | ((glyph.y & 0xFFFF) << 16));
553
- // Pack tex.w: band max + flags
554
521
  let texWBits = (sd.bandMaxX & 0xFF) | ((sd.bandMaxY & 0xFF) << 16);
555
522
  if (evenOdd)
556
523
  texWBits |= 0x10000000; // E flag at bit 28
557
524
  const texW = uintAsFloat(texWBits);
558
- // Band transform: scale to map glyph-local em-coords to band indices
559
525
  const bandScaleX = w > 0 ? sd.vBands.length / w : 0;
560
526
  const bandScaleY = h > 0 ? sd.hBands.length / h : 0;
561
527
  for (let vi = 0; vi < 4; vi++) {
@@ -570,23 +536,22 @@ function packSlugData(shapes, options) {
570
536
  vertices[base + 5] = emCorners[vi][1];
571
537
  vertices[base + 6] = texZ;
572
538
  vertices[base + 7] = texW;
573
- // jac: identity Jacobian (em-space is a pure translation of object-space)
539
+ // jac: identity (em-space is a translation of object-space)
574
540
  vertices[base + 8] = 1.0;
575
541
  vertices[base + 9] = 0.0;
576
542
  vertices[base + 10] = 0.0;
577
543
  vertices[base + 11] = 1.0;
578
- // bnd: band scale (offset is zero in glyph-local space)
544
+ // bnd: band scale (offset zero in glyph-local space)
579
545
  vertices[base + 12] = bandScaleX;
580
546
  vertices[base + 13] = bandScaleY;
581
547
  vertices[base + 14] = 0;
582
548
  vertices[base + 15] = 0;
583
- // col: white with full alpha (caller overrides via uniform or attribute)
549
+ // col
584
550
  vertices[base + 16] = 1.0;
585
551
  vertices[base + 17] = 1.0;
586
552
  vertices[base + 18] = 1.0;
587
553
  vertices[base + 19] = 1.0;
588
554
  }
589
- // Indices: two triangles per quad
590
555
  const vBase = si * 4;
591
556
  const iBase = si * 6;
592
557
  indices[iBase + 0] = vBase + 0;
@@ -795,7 +760,6 @@ function computePlaneBounds(glyphInfos) {
795
760
  }
796
761
  return { min: { x: minX, y: minY, z: 0 }, max: { x: maxX, y: maxY, z: 0 } };
797
762
  }
798
- // Public API
799
763
  function buildVectorResult(layoutHandle, ctx, options) {
800
764
  const scale = layoutHandle.layoutData.pixelsPerFontUnit;
801
765
  let cachedQuery = null;
@@ -22,8 +22,6 @@ function _interopNamespaceDefault(e) {
22
22
 
23
23
  var THREE__namespace = /*#__PURE__*/_interopNamespaceDefault(THREE);
24
24
 
25
- // Restructures the tightly-packed vertex buffer from SlugPacker into
26
- // separate per-attribute arrays for GPU attribute binding
27
25
  const FLOATS_PER_VERT = 20;
28
26
  function unpackSlugVertices(gpuData) {
29
27
  const vertCount = gpuData.shapeCount * 4;
@@ -45,7 +43,6 @@ function unpackSlugVertices(gpuData) {
45
43
  bandings[i * 4 + 1] = srcF[s + 13];
46
44
  bandings[i * 4 + 2] = srcF[s + 14];
47
45
  bandings[i * 4 + 3] = srcF[s + 15];
48
- // Unpack glyph location and band metadata from bit-packed fields
49
46
  const g0 = srcU[s + 6];
50
47
  const g1 = srcU[s + 7];
51
48
  glyphData[i * 4] = g0 & 0xFFFF;
@@ -57,7 +54,6 @@ function unpackSlugVertices(gpuData) {
57
54
  colors[i * 4 + 2] = srcF[s + 18];
58
55
  colors[i * 4 + 3] = srcF[s + 19];
59
56
  }
60
- // Per-glyph center: average of quad corner positions
61
57
  const glyphCenters = new Float32Array(vertCount * 3);
62
58
  const glyphIndices = new Float32Array(vertCount);
63
59
  for (let g = 0; g < gpuData.shapeCount; g++) {
@@ -82,23 +78,14 @@ function unpackSlugVertices(gpuData) {
82
78
 
83
79
  var fragGLSL = "#version 300 es\n// GLSL 300 es port of the Slug fragment shader\n// Eric Lengyel, MIT License, 2017\n\nprecision highp float;\nprecision highp int;\n\n#define kLogBandTextureWidth 12\n\nin vec4 v_color;\nin vec2 v_texcoord;\nflat in vec4 v_banding;\nflat in ivec4 v_glyph;\n\nuniform sampler2D curveTexture; // RGBA32F control points\nuniform highp usampler2D bandTexture; // RGBA32UI band data\n\nlayout(location = 0) out vec4 outColor;\n\nuint CalcRootCode(float y1, float y2, float y3) {\n uint i1 = floatBitsToUint(y1) >> 31u;\n uint i2 = floatBitsToUint(y2) >> 30u;\n uint i3 = floatBitsToUint(y3) >> 29u;\n\n uint shift = (i2 & 2u) | (i1 & ~2u);\n shift = (i3 & 4u) | (shift & ~4u);\n\n return (0x2E74u >> shift) & 0x0101u;\n}\n\nvec2 SolveHorizPoly(vec4 p12, vec2 p3) {\n vec2 a = p12.xy - p12.zw * 2.0 + p3;\n vec2 b = p12.xy - p12.zw;\n float ra = 1.0 / a.y;\n float rb = 0.5 / b.y;\n\n float d = sqrt(max(b.y * b.y - a.y * p12.y, 0.0));\n float t1 = (b.y - d) * ra;\n float t2 = (b.y + d) * ra;\n\n if (abs(a.y) < 1.0 / 65536.0) t1 = t2 = p12.y * rb;\n\n return vec2((a.x * t1 - b.x * 2.0) * t1 + p12.x, (a.x * t2 - b.x * 2.0) * t2 + p12.x);\n}\n\nvec2 SolveVertPoly(vec4 p12, vec2 p3) {\n vec2 a = p12.xy - p12.zw * 2.0 + p3;\n vec2 b = p12.xy - p12.zw;\n float ra = 1.0 / a.x;\n float rb = 0.5 / b.x;\n\n float d = sqrt(max(b.x * b.x - a.x * p12.x, 0.0));\n float t1 = (b.x - d) * ra;\n float t2 = (b.x + d) * ra;\n\n if (abs(a.x) < 1.0 / 65536.0) t1 = t2 = p12.x * rb;\n\n return vec2((a.y * t1 - b.y * 2.0) * t1 + p12.y, (a.y * t2 - b.y * 2.0) * t2 + p12.y);\n}\n\nivec2 CalcBandLoc(ivec2 glyphLoc, uint offset) {\n ivec2 bandLoc = ivec2(glyphLoc.x + int(offset), glyphLoc.y);\n bandLoc.y += bandLoc.x >> kLogBandTextureWidth;\n bandLoc.x &= (1 << kLogBandTextureWidth) - 1;\n return bandLoc;\n}\n\nfloat CalcCoverage(float xcov, float ycov, float xwgt, float ywgt, int flags) {\n float coverage = max(abs(xcov * xwgt + ycov * ywgt) / max(xwgt + ywgt, 1.0 / 65536.0), min(abs(xcov), abs(ycov)));\n\n#if defined(SLUG_EVENODD)\n if ((flags & 0x1000) == 0) {\n#endif\n coverage = clamp(coverage, 0.0, 1.0);\n#if defined(SLUG_EVENODD)\n } else {\n coverage = 1.0 - abs(1.0 - fract(coverage * 0.5) * 2.0);\n }\n#endif\n\n#if defined(SLUG_WEIGHT)\n coverage = sqrt(coverage);\n#endif\n\n return coverage;\n}\n\nfloat SlugRenderSingle(vec2 renderCoord, vec2 emsPerPixel, vec4 bandTransform, ivec4 glyphData) {\n int curveIndex;\n\n vec2 pixelsPerEm = 1.0 / emsPerPixel;\n\n ivec2 bandMax = glyphData.zw;\n bandMax.y &= 0x00FF;\n\n ivec2 bandIndex = clamp(ivec2(renderCoord * bandTransform.xy + bandTransform.zw), ivec2(0, 0), bandMax);\n ivec2 glyphLoc = glyphData.xy;\n\n float xcov = 0.0;\n float xwgt = 0.0;\n\n uvec2 hbandData = texelFetch(bandTexture, ivec2(glyphLoc.x + bandIndex.y, glyphLoc.y), 0).xy;\n ivec2 hbandLoc = CalcBandLoc(glyphLoc, hbandData.y);\n\n for (curveIndex = 0; curveIndex < int(hbandData.x); curveIndex++) {\n ivec2 curveLoc = ivec2(texelFetch(bandTexture, ivec2(hbandLoc.x + curveIndex, hbandLoc.y), 0).xy);\n\n vec4 p12 = texelFetch(curveTexture, curveLoc, 0) - vec4(renderCoord, renderCoord);\n vec2 p3 = texelFetch(curveTexture, ivec2(curveLoc.x + 1, curveLoc.y), 0).xy - renderCoord;\n\n if (max(max(p12.x, p12.z), p3.x) * pixelsPerEm.x < -0.5) break;\n\n uint code = CalcRootCode(p12.y, p12.w, p3.y);\n if (code != 0u) {\n vec2 r = SolveHorizPoly(p12, p3) * pixelsPerEm.x;\n\n if ((code & 1u) != 0u) {\n xcov += clamp(r.x + 0.5, 0.0, 1.0);\n xwgt = max(xwgt, clamp(1.0 - abs(r.x) * 2.0, 0.0, 1.0));\n }\n\n if (code > 1u) {\n xcov -= clamp(r.y + 0.5, 0.0, 1.0);\n xwgt = max(xwgt, clamp(1.0 - abs(r.y) * 2.0, 0.0, 1.0));\n }\n }\n }\n\n float ycov = 0.0;\n float ywgt = 0.0;\n\n uvec2 vbandData = texelFetch(bandTexture, ivec2(glyphLoc.x + bandMax.y + 1 + bandIndex.x, glyphLoc.y), 0).xy;\n ivec2 vbandLoc = CalcBandLoc(glyphLoc, vbandData.y);\n\n for (curveIndex = 0; curveIndex < int(vbandData.x); curveIndex++) {\n ivec2 curveLoc = ivec2(texelFetch(bandTexture, ivec2(vbandLoc.x + curveIndex, vbandLoc.y), 0).xy);\n vec4 p12 = texelFetch(curveTexture, curveLoc, 0) - vec4(renderCoord, renderCoord);\n vec2 p3 = texelFetch(curveTexture, ivec2(curveLoc.x + 1, curveLoc.y), 0).xy - renderCoord;\n\n if (max(max(p12.y, p12.w), p3.y) * pixelsPerEm.y < -0.5) break;\n\n uint code = CalcRootCode(p12.x, p12.z, p3.x);\n if (code != 0u) {\n vec2 r = SolveVertPoly(p12, p3) * pixelsPerEm.y;\n\n if ((code & 1u) != 0u) {\n ycov -= clamp(r.x + 0.5, 0.0, 1.0);\n ywgt = max(ywgt, clamp(1.0 - abs(r.x) * 2.0, 0.0, 1.0));\n }\n\n if (code > 1u) {\n ycov += clamp(r.y + 0.5, 0.0, 1.0);\n ywgt = max(ywgt, clamp(1.0 - abs(r.y) * 2.0, 0.0, 1.0));\n }\n }\n }\n\n return CalcCoverage(xcov, ycov, xwgt, ywgt, glyphData.w);\n}\n\nfloat SlugRender(vec2 renderCoord, vec4 bandTransform, ivec4 glyphData) {\n vec2 emsPerPixel = fwidth(renderCoord);\n\n#if defined(SLUG_ADAPTIVE_SUPERSAMPLE)\n // Per-pixel rotated RGSS-4. The base RGSS offsets are rotated by a\n // unique angle per fragment (interleaved gradient noise). This converts\n // structured aliasing shimmer into uncorrelated grain that the eye\n // naturally filters out, much closer to how hardware MSAA on many small\n // triangles behaves perceptually.\n float noise = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715))));\n float angle = noise * 6.2831853;\n float ca = cos(angle), sa = sin(angle);\n\n // Base RGSS offsets rotated by per-pixel angle\n vec2 o0 = vec2(ca * -0.375 - sa * 0.125, sa * -0.375 + ca * 0.125) * emsPerPixel;\n vec2 o1 = vec2(ca * 0.125 - sa * 0.375, sa * 0.125 + ca * 0.375) * emsPerPixel;\n vec2 o2 = vec2(ca * 0.375 - sa * -0.125, sa * 0.375 + ca * -0.125) * emsPerPixel;\n vec2 o3 = vec2(ca * -0.125 - sa * -0.375, sa * -0.125 + ca * -0.375) * emsPerPixel;\n\n float coverage =\n SlugRenderSingle(renderCoord + o0, emsPerPixel, bandTransform, glyphData) +\n SlugRenderSingle(renderCoord + o1, emsPerPixel, bandTransform, glyphData) +\n SlugRenderSingle(renderCoord + o2, emsPerPixel, bandTransform, glyphData) +\n SlugRenderSingle(renderCoord + o3, emsPerPixel, bandTransform, glyphData);\n return coverage * 0.25;\n#else\n return SlugRenderSingle(renderCoord, emsPerPixel, bandTransform, glyphData);\n#endif\n}\n\nvoid main() {\n float coverage = SlugRender(v_texcoord, v_banding, v_glyph);\n outColor = v_color * coverage;\n}\n";
84
80
 
85
- // Slug shader source re-exports
86
- // The .glsl/.wgsl files are the single source of truth, imported as strings
87
- // at build time via the glslPlugin in rollup.config.js
88
81
  // @ts-ignore - resolved by rollup glslPlugin
89
82
  const fragmentShaderGLSL300 = fragGLSL;
90
83
 
91
- // Slug GLSL adapter for Three.js, using RawShaderMaterial with the
92
- // reference GLSL shaders. Works with both WebGLRenderer and
93
- // WebGPURenderer (via GLSL-to-WGSL transpilation)
84
+ // Slug adapter using Three.js RawShaderMaterial (GLSL)
85
+ // Works with any Three.js renderer (WebGL or WebGPU via transpilation)
94
86
  //
95
- // Compared to the TSL adapter (slugTSL.ts):
96
- // - Works with any Three.js renderer (no node-material dependency)
97
- // - Uses native Uint32 band texture (no float conversion)
98
- // - Supports GLSL animation injection via animationDeclarations/animationBody
99
- // - Same tradeoff: no vertex dilation (may cause sub-pixel edge clipping at extreme zoom)
100
- //
101
- // Requires peer dependency: three
87
+ // Supports GLSL animation injection and native Uint32 band textures
88
+ // No vertex dilation; may clip at sub-pixel edges under extreme zoom
102
89
  // @ts-ignore - three is a peer dependency
103
90
  // Three.js GLSL3 mode prepends #version 300 es, so strip it from the raw shader
104
91
  const fragShader = fragmentShaderGLSL300.replace(/^#version\s+300\s+es\s*\n/, '');
@@ -149,13 +136,12 @@ function createSlugGLSLMesh(gpuData, options) {
149
136
  geo.setAttribute('glyphCenter', new THREE__namespace.Float32BufferAttribute(attrs.glyphCenters, 3));
150
137
  geo.setAttribute('glyphIndex', new THREE__namespace.Float32BufferAttribute(attrs.glyphIndices, 1));
151
138
  geo.setIndex(new THREE__namespace.BufferAttribute(gpuData.indices, 1));
152
- // Curve texture: RGBA32F
153
139
  const curveTex = new THREE__namespace.DataTexture(gpuData.curveTexture.data, gpuData.curveTexture.width, gpuData.curveTexture.height, THREE__namespace.RGBAFormat, THREE__namespace.FloatType);
154
140
  curveTex.minFilter = THREE__namespace.NearestFilter;
155
141
  curveTex.magFilter = THREE__namespace.NearestFilter;
156
142
  curveTex.generateMipmaps = false;
157
143
  curveTex.needsUpdate = true;
158
- // Band texture: native RGBA32UI (no float conversion needed)
144
+ // RGBA32UI (unlike TSL, GLSL supports integer textures natively)
159
145
  const bandTex = new THREE__namespace.DataTexture(gpuData.bandTexture.data, gpuData.bandTexture.width, gpuData.bandTexture.height);
160
146
  bandTex.format = THREE__namespace.RGBAIntegerFormat;
161
147
  bandTex.type = THREE__namespace.UnsignedIntType;
@@ -1,8 +1,6 @@
1
1
  import { Text as Text$1 } from './core/index.js';
2
2
  import * as THREE from 'three';
3
3
 
4
- // Restructures the tightly-packed vertex buffer from SlugPacker into
5
- // separate per-attribute arrays for GPU attribute binding
6
4
  const FLOATS_PER_VERT = 20;
7
5
  function unpackSlugVertices(gpuData) {
8
6
  const vertCount = gpuData.shapeCount * 4;
@@ -24,7 +22,6 @@ function unpackSlugVertices(gpuData) {
24
22
  bandings[i * 4 + 1] = srcF[s + 13];
25
23
  bandings[i * 4 + 2] = srcF[s + 14];
26
24
  bandings[i * 4 + 3] = srcF[s + 15];
27
- // Unpack glyph location and band metadata from bit-packed fields
28
25
  const g0 = srcU[s + 6];
29
26
  const g1 = srcU[s + 7];
30
27
  glyphData[i * 4] = g0 & 0xFFFF;
@@ -36,7 +33,6 @@ function unpackSlugVertices(gpuData) {
36
33
  colors[i * 4 + 2] = srcF[s + 18];
37
34
  colors[i * 4 + 3] = srcF[s + 19];
38
35
  }
39
- // Per-glyph center: average of quad corner positions
40
36
  const glyphCenters = new Float32Array(vertCount * 3);
41
37
  const glyphIndices = new Float32Array(vertCount);
42
38
  for (let g = 0; g < gpuData.shapeCount; g++) {
@@ -61,23 +57,14 @@ function unpackSlugVertices(gpuData) {
61
57
 
62
58
  var fragGLSL = "#version 300 es\n// GLSL 300 es port of the Slug fragment shader\n// Eric Lengyel, MIT License, 2017\n\nprecision highp float;\nprecision highp int;\n\n#define kLogBandTextureWidth 12\n\nin vec4 v_color;\nin vec2 v_texcoord;\nflat in vec4 v_banding;\nflat in ivec4 v_glyph;\n\nuniform sampler2D curveTexture; // RGBA32F control points\nuniform highp usampler2D bandTexture; // RGBA32UI band data\n\nlayout(location = 0) out vec4 outColor;\n\nuint CalcRootCode(float y1, float y2, float y3) {\n uint i1 = floatBitsToUint(y1) >> 31u;\n uint i2 = floatBitsToUint(y2) >> 30u;\n uint i3 = floatBitsToUint(y3) >> 29u;\n\n uint shift = (i2 & 2u) | (i1 & ~2u);\n shift = (i3 & 4u) | (shift & ~4u);\n\n return (0x2E74u >> shift) & 0x0101u;\n}\n\nvec2 SolveHorizPoly(vec4 p12, vec2 p3) {\n vec2 a = p12.xy - p12.zw * 2.0 + p3;\n vec2 b = p12.xy - p12.zw;\n float ra = 1.0 / a.y;\n float rb = 0.5 / b.y;\n\n float d = sqrt(max(b.y * b.y - a.y * p12.y, 0.0));\n float t1 = (b.y - d) * ra;\n float t2 = (b.y + d) * ra;\n\n if (abs(a.y) < 1.0 / 65536.0) t1 = t2 = p12.y * rb;\n\n return vec2((a.x * t1 - b.x * 2.0) * t1 + p12.x, (a.x * t2 - b.x * 2.0) * t2 + p12.x);\n}\n\nvec2 SolveVertPoly(vec4 p12, vec2 p3) {\n vec2 a = p12.xy - p12.zw * 2.0 + p3;\n vec2 b = p12.xy - p12.zw;\n float ra = 1.0 / a.x;\n float rb = 0.5 / b.x;\n\n float d = sqrt(max(b.x * b.x - a.x * p12.x, 0.0));\n float t1 = (b.x - d) * ra;\n float t2 = (b.x + d) * ra;\n\n if (abs(a.x) < 1.0 / 65536.0) t1 = t2 = p12.x * rb;\n\n return vec2((a.y * t1 - b.y * 2.0) * t1 + p12.y, (a.y * t2 - b.y * 2.0) * t2 + p12.y);\n}\n\nivec2 CalcBandLoc(ivec2 glyphLoc, uint offset) {\n ivec2 bandLoc = ivec2(glyphLoc.x + int(offset), glyphLoc.y);\n bandLoc.y += bandLoc.x >> kLogBandTextureWidth;\n bandLoc.x &= (1 << kLogBandTextureWidth) - 1;\n return bandLoc;\n}\n\nfloat CalcCoverage(float xcov, float ycov, float xwgt, float ywgt, int flags) {\n float coverage = max(abs(xcov * xwgt + ycov * ywgt) / max(xwgt + ywgt, 1.0 / 65536.0), min(abs(xcov), abs(ycov)));\n\n#if defined(SLUG_EVENODD)\n if ((flags & 0x1000) == 0) {\n#endif\n coverage = clamp(coverage, 0.0, 1.0);\n#if defined(SLUG_EVENODD)\n } else {\n coverage = 1.0 - abs(1.0 - fract(coverage * 0.5) * 2.0);\n }\n#endif\n\n#if defined(SLUG_WEIGHT)\n coverage = sqrt(coverage);\n#endif\n\n return coverage;\n}\n\nfloat SlugRenderSingle(vec2 renderCoord, vec2 emsPerPixel, vec4 bandTransform, ivec4 glyphData) {\n int curveIndex;\n\n vec2 pixelsPerEm = 1.0 / emsPerPixel;\n\n ivec2 bandMax = glyphData.zw;\n bandMax.y &= 0x00FF;\n\n ivec2 bandIndex = clamp(ivec2(renderCoord * bandTransform.xy + bandTransform.zw), ivec2(0, 0), bandMax);\n ivec2 glyphLoc = glyphData.xy;\n\n float xcov = 0.0;\n float xwgt = 0.0;\n\n uvec2 hbandData = texelFetch(bandTexture, ivec2(glyphLoc.x + bandIndex.y, glyphLoc.y), 0).xy;\n ivec2 hbandLoc = CalcBandLoc(glyphLoc, hbandData.y);\n\n for (curveIndex = 0; curveIndex < int(hbandData.x); curveIndex++) {\n ivec2 curveLoc = ivec2(texelFetch(bandTexture, ivec2(hbandLoc.x + curveIndex, hbandLoc.y), 0).xy);\n\n vec4 p12 = texelFetch(curveTexture, curveLoc, 0) - vec4(renderCoord, renderCoord);\n vec2 p3 = texelFetch(curveTexture, ivec2(curveLoc.x + 1, curveLoc.y), 0).xy - renderCoord;\n\n if (max(max(p12.x, p12.z), p3.x) * pixelsPerEm.x < -0.5) break;\n\n uint code = CalcRootCode(p12.y, p12.w, p3.y);\n if (code != 0u) {\n vec2 r = SolveHorizPoly(p12, p3) * pixelsPerEm.x;\n\n if ((code & 1u) != 0u) {\n xcov += clamp(r.x + 0.5, 0.0, 1.0);\n xwgt = max(xwgt, clamp(1.0 - abs(r.x) * 2.0, 0.0, 1.0));\n }\n\n if (code > 1u) {\n xcov -= clamp(r.y + 0.5, 0.0, 1.0);\n xwgt = max(xwgt, clamp(1.0 - abs(r.y) * 2.0, 0.0, 1.0));\n }\n }\n }\n\n float ycov = 0.0;\n float ywgt = 0.0;\n\n uvec2 vbandData = texelFetch(bandTexture, ivec2(glyphLoc.x + bandMax.y + 1 + bandIndex.x, glyphLoc.y), 0).xy;\n ivec2 vbandLoc = CalcBandLoc(glyphLoc, vbandData.y);\n\n for (curveIndex = 0; curveIndex < int(vbandData.x); curveIndex++) {\n ivec2 curveLoc = ivec2(texelFetch(bandTexture, ivec2(vbandLoc.x + curveIndex, vbandLoc.y), 0).xy);\n vec4 p12 = texelFetch(curveTexture, curveLoc, 0) - vec4(renderCoord, renderCoord);\n vec2 p3 = texelFetch(curveTexture, ivec2(curveLoc.x + 1, curveLoc.y), 0).xy - renderCoord;\n\n if (max(max(p12.y, p12.w), p3.y) * pixelsPerEm.y < -0.5) break;\n\n uint code = CalcRootCode(p12.x, p12.z, p3.x);\n if (code != 0u) {\n vec2 r = SolveVertPoly(p12, p3) * pixelsPerEm.y;\n\n if ((code & 1u) != 0u) {\n ycov -= clamp(r.x + 0.5, 0.0, 1.0);\n ywgt = max(ywgt, clamp(1.0 - abs(r.x) * 2.0, 0.0, 1.0));\n }\n\n if (code > 1u) {\n ycov += clamp(r.y + 0.5, 0.0, 1.0);\n ywgt = max(ywgt, clamp(1.0 - abs(r.y) * 2.0, 0.0, 1.0));\n }\n }\n }\n\n return CalcCoverage(xcov, ycov, xwgt, ywgt, glyphData.w);\n}\n\nfloat SlugRender(vec2 renderCoord, vec4 bandTransform, ivec4 glyphData) {\n vec2 emsPerPixel = fwidth(renderCoord);\n\n#if defined(SLUG_ADAPTIVE_SUPERSAMPLE)\n // Per-pixel rotated RGSS-4. The base RGSS offsets are rotated by a\n // unique angle per fragment (interleaved gradient noise). This converts\n // structured aliasing shimmer into uncorrelated grain that the eye\n // naturally filters out, much closer to how hardware MSAA on many small\n // triangles behaves perceptually.\n float noise = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715))));\n float angle = noise * 6.2831853;\n float ca = cos(angle), sa = sin(angle);\n\n // Base RGSS offsets rotated by per-pixel angle\n vec2 o0 = vec2(ca * -0.375 - sa * 0.125, sa * -0.375 + ca * 0.125) * emsPerPixel;\n vec2 o1 = vec2(ca * 0.125 - sa * 0.375, sa * 0.125 + ca * 0.375) * emsPerPixel;\n vec2 o2 = vec2(ca * 0.375 - sa * -0.125, sa * 0.375 + ca * -0.125) * emsPerPixel;\n vec2 o3 = vec2(ca * -0.125 - sa * -0.375, sa * -0.125 + ca * -0.375) * emsPerPixel;\n\n float coverage =\n SlugRenderSingle(renderCoord + o0, emsPerPixel, bandTransform, glyphData) +\n SlugRenderSingle(renderCoord + o1, emsPerPixel, bandTransform, glyphData) +\n SlugRenderSingle(renderCoord + o2, emsPerPixel, bandTransform, glyphData) +\n SlugRenderSingle(renderCoord + o3, emsPerPixel, bandTransform, glyphData);\n return coverage * 0.25;\n#else\n return SlugRenderSingle(renderCoord, emsPerPixel, bandTransform, glyphData);\n#endif\n}\n\nvoid main() {\n float coverage = SlugRender(v_texcoord, v_banding, v_glyph);\n outColor = v_color * coverage;\n}\n";
63
59
 
64
- // Slug shader source re-exports
65
- // The .glsl/.wgsl files are the single source of truth, imported as strings
66
- // at build time via the glslPlugin in rollup.config.js
67
60
  // @ts-ignore - resolved by rollup glslPlugin
68
61
  const fragmentShaderGLSL300 = fragGLSL;
69
62
 
70
- // Slug GLSL adapter for Three.js, using RawShaderMaterial with the
71
- // reference GLSL shaders. Works with both WebGLRenderer and
72
- // WebGPURenderer (via GLSL-to-WGSL transpilation)
63
+ // Slug adapter using Three.js RawShaderMaterial (GLSL)
64
+ // Works with any Three.js renderer (WebGL or WebGPU via transpilation)
73
65
  //
74
- // Compared to the TSL adapter (slugTSL.ts):
75
- // - Works with any Three.js renderer (no node-material dependency)
76
- // - Uses native Uint32 band texture (no float conversion)
77
- // - Supports GLSL animation injection via animationDeclarations/animationBody
78
- // - Same tradeoff: no vertex dilation (may cause sub-pixel edge clipping at extreme zoom)
79
- //
80
- // Requires peer dependency: three
66
+ // Supports GLSL animation injection and native Uint32 band textures
67
+ // No vertex dilation; may clip at sub-pixel edges under extreme zoom
81
68
  // @ts-ignore - three is a peer dependency
82
69
  // Three.js GLSL3 mode prepends #version 300 es, so strip it from the raw shader
83
70
  const fragShader = fragmentShaderGLSL300.replace(/^#version\s+300\s+es\s*\n/, '');
@@ -128,13 +115,12 @@ function createSlugGLSLMesh(gpuData, options) {
128
115
  geo.setAttribute('glyphCenter', new THREE.Float32BufferAttribute(attrs.glyphCenters, 3));
129
116
  geo.setAttribute('glyphIndex', new THREE.Float32BufferAttribute(attrs.glyphIndices, 1));
130
117
  geo.setIndex(new THREE.BufferAttribute(gpuData.indices, 1));
131
- // Curve texture: RGBA32F
132
118
  const curveTex = new THREE.DataTexture(gpuData.curveTexture.data, gpuData.curveTexture.width, gpuData.curveTexture.height, THREE.RGBAFormat, THREE.FloatType);
133
119
  curveTex.minFilter = THREE.NearestFilter;
134
120
  curveTex.magFilter = THREE.NearestFilter;
135
121
  curveTex.generateMipmaps = false;
136
122
  curveTex.needsUpdate = true;
137
- // Band texture: native RGBA32UI (no float conversion needed)
123
+ // RGBA32UI (unlike TSL, GLSL supports integer textures natively)
138
124
  const bandTex = new THREE.DataTexture(gpuData.bandTexture.data, gpuData.bandTexture.width, gpuData.bandTexture.height);
139
125
  bandTex.format = THREE.RGBAIntegerFormat;
140
126
  bandTex.type = THREE.UnsignedIntType;
@@ -25,20 +25,11 @@ function _interopNamespaceDefault(e) {
25
25
 
26
26
  var THREE__namespace = /*#__PURE__*/_interopNamespaceDefault(THREE);
27
27
 
28
- // Slug TSL adapter for Three.js WebGPURenderer (and WebGL via r170+)
28
+ // Slug adapter using Three.js TSL (node materials)
29
+ // Works on both WebGPU and WebGL backends (r170+)
29
30
  //
30
- // Creates a single Three.js Mesh with a NodeMaterial that implements
31
- // the Slug algorithm: per-fragment winding number evaluation via
32
- // band-accelerated ray-curve intersection
33
- //
34
- // Works on both WebGPU and WebGL backends via Three.js TSL
35
- //
36
- // Compared to the raw GLSL/WGSL standalone renderers, this adapter
37
- // trades some features for Three.js integration:
38
- // - No vertex dilation (may cause sub-pixel edge clipping at extreme zoom)
39
- // - No adaptive supersampling (single-sample per fragment)
40
- //
41
- // Requires peer dependencies: three, three/tsl
31
+ // No vertex dilation or adaptive supersampling (unlike the raw
32
+ // GLSL/WGSL renderers); may clip at sub-pixel edges under extreme zoom
42
33
  // @ts-ignore - three is a peer dependency
43
34
  const LOG_BAND_TEX_W = 12;
44
35
  const BAND_TEX_W_MASK = (1 << LOG_BAND_TEX_W) - 1;
@@ -100,10 +91,6 @@ const calcBandLoc = tsl.Fn(([glyphX, glyphY, offset]) => {
100
91
  bx.assign(bx.bitAnd(BAND_TEX_W_MASK));
101
92
  return tsl.ivec2(bx, by);
102
93
  });
103
- // Create a Three.js Mesh from SlugGPUData using TSL node materials.
104
- // Returns a single transparent mesh suitable for any Three.js scene.
105
- // The Slug algorithm evaluates per-fragment coverage analytically,
106
- // so no stencil buffer or multi-pass rendering is required
107
94
  function createSlugTSLMesh(gpuData, color) {
108
95
  const attrs = index.unpackSlugVertices(gpuData);
109
96
  const geo = new THREE__namespace.BufferGeometry();
@@ -130,16 +117,11 @@ function createSlugTSLMesh(gpuData, color) {
130
117
  bandTex.magFilter = THREE__namespace.NearestFilter;
131
118
  bandTex.generateMipmaps = false;
132
119
  bandTex.needsUpdate = true;
133
- // Varyings: vertex attributes interpolated to fragment stage
134
120
  const vTexcoord = tsl.varying(tsl.attribute('slugTexcoord', 'vec2'), 'v_texcoord');
135
121
  const vBanding = tsl.varying(tsl.attribute('slugBanding', 'vec4'), 'v_banding');
136
122
  const vGlyph = tsl.varying(tsl.attribute('slugGlyph', 'vec4'), 'v_glyph');
137
123
  const vColor = tsl.varying(tsl.attribute('slugColor', 'vec4'), 'v_color');
138
- // Color uniform (allows dynamic color updates)
139
124
  const textColor = tsl.uniform(new THREE__namespace.Color(color?.r ?? 1, color?.g ?? 1, color?.b ?? 1));
140
- // Main per-fragment evaluation: SlugRenderSingle ported to TSL
141
- // Evaluates horizontal and vertical band loops to compute
142
- // analytic winding-number coverage
143
125
  const slugRenderSingle = tsl.Fn(([renderCoord, emsPerPixel, bandTransform, glyphData]) => {
144
126
  const pixelsPerEm = tsl.vec2(tsl.float(1).div(emsPerPixel.x), tsl.float(1).div(emsPerPixel.y));
145
127
  const glyphLocX = glyphData.x.toInt();
@@ -148,7 +130,6 @@ function createSlugTSLMesh(gpuData, color) {
148
130
  const bandMaxY = glyphData.w.toInt().bitAnd(0xFF);
149
131
  const bandIdxX = tsl.max(tsl.min(renderCoord.x.mul(bandTransform.x).add(bandTransform.z).toInt(), bandMaxX), tsl.int(0));
150
132
  const bandIdxY = tsl.max(tsl.min(renderCoord.y.mul(bandTransform.y).add(bandTransform.w).toInt(), bandMaxY), tsl.int(0));
151
- // Horizontal band loop
152
133
  const xcov = tsl.float(0).toVar();
153
134
  const xwgt = tsl.float(0).toVar();
154
135
  const hbandData = tsl.textureLoad(bandTex, tsl.ivec2(glyphLocX.add(bandIdxY), glyphLocY));
@@ -181,7 +162,6 @@ function createSlugTSLMesh(gpuData, color) {
181
162
  });
182
163
  hIdx.addAssign(1);
183
164
  });
184
- // Vertical band loop
185
165
  const ycov = tsl.float(0).toVar();
186
166
  const ywgt = tsl.float(0).toVar();
187
167
  const vbandOffset = bandMaxY.add(1).add(bandIdxX);
@@ -215,17 +195,14 @@ function createSlugTSLMesh(gpuData, color) {
215
195
  });
216
196
  vIdx.addAssign(1);
217
197
  });
218
- // CalcCoverage (nonzero winding rule)
219
198
  const coverage = tsl.max(tsl.abs(xcov.mul(xwgt).add(ycov.mul(ywgt))).div(tsl.max(xwgt.add(ywgt), tsl.float(1.0 / 65536.0))), tsl.min(tsl.abs(xcov), tsl.abs(ycov)));
220
199
  return tsl.clamp(coverage, 0, 1);
221
200
  });
222
- // Top-level fragment node
223
201
  const fragmentNode = tsl.Fn(() => {
224
202
  const emsPerPixel = tsl.fwidth(vTexcoord);
225
203
  const coverage = slugRenderSingle(vTexcoord, emsPerPixel, vBanding, vGlyph);
226
204
  return tsl.vec4(textColor.x, textColor.y, textColor.z, vColor.w.mul(coverage));
227
205
  })();
228
- // Material & mesh
229
206
  const material = new webgpu.MeshBasicNodeMaterial();
230
207
  material.fragmentNode = fragmentNode;
231
208
  material.transparent = true;
@@ -4,20 +4,11 @@ import { Fn, select, uint, float, sqrt, max, If, abs, vec2, ivec2, varying, attr
4
4
  import { u as unpackSlugVertices } from './index2.js';
5
5
  import './core/index.js';
6
6
 
7
- // Slug TSL adapter for Three.js WebGPURenderer (and WebGL via r170+)
7
+ // Slug adapter using Three.js TSL (node materials)
8
+ // Works on both WebGPU and WebGL backends (r170+)
8
9
  //
9
- // Creates a single Three.js Mesh with a NodeMaterial that implements
10
- // the Slug algorithm: per-fragment winding number evaluation via
11
- // band-accelerated ray-curve intersection
12
- //
13
- // Works on both WebGPU and WebGL backends via Three.js TSL
14
- //
15
- // Compared to the raw GLSL/WGSL standalone renderers, this adapter
16
- // trades some features for Three.js integration:
17
- // - No vertex dilation (may cause sub-pixel edge clipping at extreme zoom)
18
- // - No adaptive supersampling (single-sample per fragment)
19
- //
20
- // Requires peer dependencies: three, three/tsl
10
+ // No vertex dilation or adaptive supersampling (unlike the raw
11
+ // GLSL/WGSL renderers); may clip at sub-pixel edges under extreme zoom
21
12
  // @ts-ignore - three is a peer dependency
22
13
  const LOG_BAND_TEX_W = 12;
23
14
  const BAND_TEX_W_MASK = (1 << LOG_BAND_TEX_W) - 1;
@@ -79,10 +70,6 @@ const calcBandLoc = Fn(([glyphX, glyphY, offset]) => {
79
70
  bx.assign(bx.bitAnd(BAND_TEX_W_MASK));
80
71
  return ivec2(bx, by);
81
72
  });
82
- // Create a Three.js Mesh from SlugGPUData using TSL node materials.
83
- // Returns a single transparent mesh suitable for any Three.js scene.
84
- // The Slug algorithm evaluates per-fragment coverage analytically,
85
- // so no stencil buffer or multi-pass rendering is required
86
73
  function createSlugTSLMesh(gpuData, color) {
87
74
  const attrs = unpackSlugVertices(gpuData);
88
75
  const geo = new THREE.BufferGeometry();
@@ -109,16 +96,11 @@ function createSlugTSLMesh(gpuData, color) {
109
96
  bandTex.magFilter = THREE.NearestFilter;
110
97
  bandTex.generateMipmaps = false;
111
98
  bandTex.needsUpdate = true;
112
- // Varyings: vertex attributes interpolated to fragment stage
113
99
  const vTexcoord = varying(attribute('slugTexcoord', 'vec2'), 'v_texcoord');
114
100
  const vBanding = varying(attribute('slugBanding', 'vec4'), 'v_banding');
115
101
  const vGlyph = varying(attribute('slugGlyph', 'vec4'), 'v_glyph');
116
102
  const vColor = varying(attribute('slugColor', 'vec4'), 'v_color');
117
- // Color uniform (allows dynamic color updates)
118
103
  const textColor = uniform(new THREE.Color(color?.r ?? 1, color?.g ?? 1, color?.b ?? 1));
119
- // Main per-fragment evaluation: SlugRenderSingle ported to TSL
120
- // Evaluates horizontal and vertical band loops to compute
121
- // analytic winding-number coverage
122
104
  const slugRenderSingle = Fn(([renderCoord, emsPerPixel, bandTransform, glyphData]) => {
123
105
  const pixelsPerEm = vec2(float(1).div(emsPerPixel.x), float(1).div(emsPerPixel.y));
124
106
  const glyphLocX = glyphData.x.toInt();
@@ -127,7 +109,6 @@ function createSlugTSLMesh(gpuData, color) {
127
109
  const bandMaxY = glyphData.w.toInt().bitAnd(0xFF);
128
110
  const bandIdxX = max(min(renderCoord.x.mul(bandTransform.x).add(bandTransform.z).toInt(), bandMaxX), int(0));
129
111
  const bandIdxY = max(min(renderCoord.y.mul(bandTransform.y).add(bandTransform.w).toInt(), bandMaxY), int(0));
130
- // Horizontal band loop
131
112
  const xcov = float(0).toVar();
132
113
  const xwgt = float(0).toVar();
133
114
  const hbandData = textureLoad(bandTex, ivec2(glyphLocX.add(bandIdxY), glyphLocY));
@@ -160,7 +141,6 @@ function createSlugTSLMesh(gpuData, color) {
160
141
  });
161
142
  hIdx.addAssign(1);
162
143
  });
163
- // Vertical band loop
164
144
  const ycov = float(0).toVar();
165
145
  const ywgt = float(0).toVar();
166
146
  const vbandOffset = bandMaxY.add(1).add(bandIdxX);
@@ -194,17 +174,14 @@ function createSlugTSLMesh(gpuData, color) {
194
174
  });
195
175
  vIdx.addAssign(1);
196
176
  });
197
- // CalcCoverage (nonzero winding rule)
198
177
  const coverage = max(abs(xcov.mul(xwgt).add(ycov.mul(ywgt))).div(max(xwgt.add(ywgt), float(1.0 / 65536.0))), min(abs(xcov), abs(ycov)));
199
178
  return clamp(coverage, 0, 1);
200
179
  });
201
- // Top-level fragment node
202
180
  const fragmentNode = Fn(() => {
203
181
  const emsPerPixel = fwidth(vTexcoord);
204
182
  const coverage = slugRenderSingle(vTexcoord, emsPerPixel, vBanding, vGlyph);
205
183
  return vec4(textColor.x, textColor.y, textColor.z, vColor.w.mul(coverage));
206
184
  })();
207
- // Material & mesh
208
185
  const material = new MeshBasicNodeMaterial();
209
186
  material.fragmentNode = fragmentNode;
210
187
  material.transparent = true;
@@ -1,12 +1,9 @@
1
1
  'use strict';
2
2
 
3
- var vertGLSL = "#version 300 es\n// GLSL 300 es port of the Slug vertex shader\n// Eric Lengyel, MIT License, 2017\n\nprecision highp float;\nprecision highp int;\n\n// Per-vertex attributes (5 x vec4, matching Slug reference layout)\nlayout(location = 0) in vec4 a_pos; // .xy = object-space position, .zw = outward normal\nlayout(location = 1) in vec4 a_tex; // .xy = em-space sample coords, .z = packed glyph loc, .w = packed band max + flags\nlayout(location = 2) in vec4 a_jac; // inverse Jacobian (2x2): (j00, j01, j10, j11)\nlayout(location = 3) in vec4 a_bnd; // (bandScaleX, bandScaleY, bandOffsetX, bandOffsetY)\nlayout(location = 4) in vec4 a_col; // vertex color RGBA\n\nuniform mat4 slug_matrix; // MVP matrix (rows as vec4s)\nuniform vec2 slug_viewport; // viewport dimensions in pixels\n\nout vec4 v_color;\nout vec2 v_texcoord;\nflat out vec4 v_banding;\nflat out ivec4 v_glyph;\n\nvoid SlugUnpack(vec4 tex, vec4 bnd, out vec4 vbnd, out ivec4 vgly) {\n uvec2 g = floatBitsToUint(tex.zw);\n vgly = ivec4(g.x & 0xFFFFu, g.x >> 16u, g.y & 0xFFFFu, g.y >> 16u);\n vbnd = bnd;\n}\n\nvec2 SlugDilate(vec4 pos, vec4 tex, vec4 jac, vec4 m0, vec4 m1, vec4 m3, vec2 dim, out vec2 vpos) {\n vec2 n = normalize(pos.zw);\n float s = dot(m3.xy, pos.xy) + m3.w;\n float t = dot(m3.xy, n);\n\n float u = (s * dot(m0.xy, n) - t * (dot(m0.xy, pos.xy) + m0.w)) * dim.x;\n float v = (s * dot(m1.xy, n) - t * (dot(m1.xy, pos.xy) + m1.w)) * dim.y;\n\n float s2 = s * s;\n float st = s * t;\n float uv = u * u + v * v;\n vec2 d = pos.zw * (s2 * (st + sqrt(uv)) / (uv - st * st));\n\n vpos = pos.xy + d;\n return vec2(tex.x + dot(d, jac.xy), tex.y + dot(d, jac.zw));\n}\n\nvoid main() {\n vec2 p;\n\n // Dynamic dilation: expand quad by a pixel to prevent edge clipping.\n v_texcoord = SlugDilate(a_pos, a_tex, a_jac,\n slug_matrix[0], slug_matrix[1], slug_matrix[3],\n slug_viewport, p);\n\n // MVP transform on dilated position.\n gl_Position.x = p.x * slug_matrix[0].x + p.y * slug_matrix[0].y + slug_matrix[0].w;\n gl_Position.y = p.x * slug_matrix[1].x + p.y * slug_matrix[1].y + slug_matrix[1].w;\n gl_Position.z = p.x * slug_matrix[2].x + p.y * slug_matrix[2].y + slug_matrix[2].w;\n gl_Position.w = p.x * slug_matrix[3].x + p.y * slug_matrix[3].y + slug_matrix[3].w;\n\n SlugUnpack(a_tex, a_bnd, v_banding, v_glyph);\n v_color = a_col;\n}\n";
3
+ var vertGLSL = "#version 300 es\n// GLSL 300 es port of the Slug vertex shader\n// Eric Lengyel, MIT License, 2017\n\nprecision highp float;\nprecision highp int;\n\n// Per-vertex attributes (5 x vec4, matching Slug reference layout)\nlayout(location = 0) in vec4 a_pos; // .xy = object-space position, .zw = outward normal\nlayout(location = 1) in vec4 a_tex; // .xy = em-space sample coords, .z = packed glyph loc, .w = packed band max + flags\nlayout(location = 2) in vec4 a_jac; // inverse Jacobian (2x2): (j00, j01, j10, j11)\nlayout(location = 3) in vec4 a_bnd; // (bandScaleX, bandScaleY, bandOffsetX, bandOffsetY)\nlayout(location = 4) in vec4 a_col; // vertex color RGBA\n\nuniform mat4 slug_matrix; // MVP matrix (rows as vec4s)\nuniform vec2 slug_viewport; // viewport dimensions in pixels\n\nout vec4 v_color;\nout vec2 v_texcoord;\nflat out vec4 v_banding;\nflat out ivec4 v_glyph;\n\nvoid SlugUnpack(vec4 tex, vec4 bnd, out vec4 vbnd, out ivec4 vgly) {\n uvec2 g = floatBitsToUint(tex.zw);\n vgly = ivec4(g.x & 0xFFFFu, g.x >> 16u, g.y & 0xFFFFu, g.y >> 16u);\n vbnd = bnd;\n}\n\nvec2 SlugDilate(vec4 pos, vec4 tex, vec4 jac, vec4 m0, vec4 m1, vec4 m3, vec2 dim, out vec2 vpos) {\n vec2 n = normalize(pos.zw);\n float s = dot(m3.xy, pos.xy) + m3.w;\n float t = dot(m3.xy, n);\n\n float u = (s * dot(m0.xy, n) - t * (dot(m0.xy, pos.xy) + m0.w)) * dim.x;\n float v = (s * dot(m1.xy, n) - t * (dot(m1.xy, pos.xy) + m1.w)) * dim.y;\n\n float s2 = s * s;\n float st = s * t;\n float uv = u * u + v * v;\n vec2 d = pos.zw * (s2 * (st + sqrt(uv)) / (uv - st * st));\n\n vpos = pos.xy + d;\n return vec2(tex.x + dot(d, jac.xy), tex.y + dot(d, jac.zw));\n}\n\nvoid main() {\n vec2 p;\n\n // Dynamic dilation: expand quad by a pixel to prevent edge clipping\n v_texcoord = SlugDilate(a_pos, a_tex, a_jac,\n slug_matrix[0], slug_matrix[1], slug_matrix[3],\n slug_viewport, p);\n\n // MVP transform on dilated position\n gl_Position.x = p.x * slug_matrix[0].x + p.y * slug_matrix[0].y + slug_matrix[0].w;\n gl_Position.y = p.x * slug_matrix[1].x + p.y * slug_matrix[1].y + slug_matrix[1].w;\n gl_Position.z = p.x * slug_matrix[2].x + p.y * slug_matrix[2].y + slug_matrix[2].w;\n gl_Position.w = p.x * slug_matrix[3].x + p.y * slug_matrix[3].y + slug_matrix[3].w;\n\n SlugUnpack(a_tex, a_bnd, v_banding, v_glyph);\n v_color = a_col;\n}\n";
4
4
 
5
5
  var fragGLSL = "#version 300 es\n// GLSL 300 es port of the Slug fragment shader\n// Eric Lengyel, MIT License, 2017\n\nprecision highp float;\nprecision highp int;\n\n#define kLogBandTextureWidth 12\n\nin vec4 v_color;\nin vec2 v_texcoord;\nflat in vec4 v_banding;\nflat in ivec4 v_glyph;\n\nuniform sampler2D curveTexture; // RGBA32F control points\nuniform highp usampler2D bandTexture; // RGBA32UI band data\n\nlayout(location = 0) out vec4 outColor;\n\nuint CalcRootCode(float y1, float y2, float y3) {\n uint i1 = floatBitsToUint(y1) >> 31u;\n uint i2 = floatBitsToUint(y2) >> 30u;\n uint i3 = floatBitsToUint(y3) >> 29u;\n\n uint shift = (i2 & 2u) | (i1 & ~2u);\n shift = (i3 & 4u) | (shift & ~4u);\n\n return (0x2E74u >> shift) & 0x0101u;\n}\n\nvec2 SolveHorizPoly(vec4 p12, vec2 p3) {\n vec2 a = p12.xy - p12.zw * 2.0 + p3;\n vec2 b = p12.xy - p12.zw;\n float ra = 1.0 / a.y;\n float rb = 0.5 / b.y;\n\n float d = sqrt(max(b.y * b.y - a.y * p12.y, 0.0));\n float t1 = (b.y - d) * ra;\n float t2 = (b.y + d) * ra;\n\n if (abs(a.y) < 1.0 / 65536.0) t1 = t2 = p12.y * rb;\n\n return vec2((a.x * t1 - b.x * 2.0) * t1 + p12.x, (a.x * t2 - b.x * 2.0) * t2 + p12.x);\n}\n\nvec2 SolveVertPoly(vec4 p12, vec2 p3) {\n vec2 a = p12.xy - p12.zw * 2.0 + p3;\n vec2 b = p12.xy - p12.zw;\n float ra = 1.0 / a.x;\n float rb = 0.5 / b.x;\n\n float d = sqrt(max(b.x * b.x - a.x * p12.x, 0.0));\n float t1 = (b.x - d) * ra;\n float t2 = (b.x + d) * ra;\n\n if (abs(a.x) < 1.0 / 65536.0) t1 = t2 = p12.x * rb;\n\n return vec2((a.y * t1 - b.y * 2.0) * t1 + p12.y, (a.y * t2 - b.y * 2.0) * t2 + p12.y);\n}\n\nivec2 CalcBandLoc(ivec2 glyphLoc, uint offset) {\n ivec2 bandLoc = ivec2(glyphLoc.x + int(offset), glyphLoc.y);\n bandLoc.y += bandLoc.x >> kLogBandTextureWidth;\n bandLoc.x &= (1 << kLogBandTextureWidth) - 1;\n return bandLoc;\n}\n\nfloat CalcCoverage(float xcov, float ycov, float xwgt, float ywgt, int flags) {\n float coverage = max(abs(xcov * xwgt + ycov * ywgt) / max(xwgt + ywgt, 1.0 / 65536.0), min(abs(xcov), abs(ycov)));\n\n#if defined(SLUG_EVENODD)\n if ((flags & 0x1000) == 0) {\n#endif\n coverage = clamp(coverage, 0.0, 1.0);\n#if defined(SLUG_EVENODD)\n } else {\n coverage = 1.0 - abs(1.0 - fract(coverage * 0.5) * 2.0);\n }\n#endif\n\n#if defined(SLUG_WEIGHT)\n coverage = sqrt(coverage);\n#endif\n\n return coverage;\n}\n\nfloat SlugRenderSingle(vec2 renderCoord, vec2 emsPerPixel, vec4 bandTransform, ivec4 glyphData) {\n int curveIndex;\n\n vec2 pixelsPerEm = 1.0 / emsPerPixel;\n\n ivec2 bandMax = glyphData.zw;\n bandMax.y &= 0x00FF;\n\n ivec2 bandIndex = clamp(ivec2(renderCoord * bandTransform.xy + bandTransform.zw), ivec2(0, 0), bandMax);\n ivec2 glyphLoc = glyphData.xy;\n\n float xcov = 0.0;\n float xwgt = 0.0;\n\n uvec2 hbandData = texelFetch(bandTexture, ivec2(glyphLoc.x + bandIndex.y, glyphLoc.y), 0).xy;\n ivec2 hbandLoc = CalcBandLoc(glyphLoc, hbandData.y);\n\n for (curveIndex = 0; curveIndex < int(hbandData.x); curveIndex++) {\n ivec2 curveLoc = ivec2(texelFetch(bandTexture, ivec2(hbandLoc.x + curveIndex, hbandLoc.y), 0).xy);\n\n vec4 p12 = texelFetch(curveTexture, curveLoc, 0) - vec4(renderCoord, renderCoord);\n vec2 p3 = texelFetch(curveTexture, ivec2(curveLoc.x + 1, curveLoc.y), 0).xy - renderCoord;\n\n if (max(max(p12.x, p12.z), p3.x) * pixelsPerEm.x < -0.5) break;\n\n uint code = CalcRootCode(p12.y, p12.w, p3.y);\n if (code != 0u) {\n vec2 r = SolveHorizPoly(p12, p3) * pixelsPerEm.x;\n\n if ((code & 1u) != 0u) {\n xcov += clamp(r.x + 0.5, 0.0, 1.0);\n xwgt = max(xwgt, clamp(1.0 - abs(r.x) * 2.0, 0.0, 1.0));\n }\n\n if (code > 1u) {\n xcov -= clamp(r.y + 0.5, 0.0, 1.0);\n xwgt = max(xwgt, clamp(1.0 - abs(r.y) * 2.0, 0.0, 1.0));\n }\n }\n }\n\n float ycov = 0.0;\n float ywgt = 0.0;\n\n uvec2 vbandData = texelFetch(bandTexture, ivec2(glyphLoc.x + bandMax.y + 1 + bandIndex.x, glyphLoc.y), 0).xy;\n ivec2 vbandLoc = CalcBandLoc(glyphLoc, vbandData.y);\n\n for (curveIndex = 0; curveIndex < int(vbandData.x); curveIndex++) {\n ivec2 curveLoc = ivec2(texelFetch(bandTexture, ivec2(vbandLoc.x + curveIndex, vbandLoc.y), 0).xy);\n vec4 p12 = texelFetch(curveTexture, curveLoc, 0) - vec4(renderCoord, renderCoord);\n vec2 p3 = texelFetch(curveTexture, ivec2(curveLoc.x + 1, curveLoc.y), 0).xy - renderCoord;\n\n if (max(max(p12.y, p12.w), p3.y) * pixelsPerEm.y < -0.5) break;\n\n uint code = CalcRootCode(p12.x, p12.z, p3.x);\n if (code != 0u) {\n vec2 r = SolveVertPoly(p12, p3) * pixelsPerEm.y;\n\n if ((code & 1u) != 0u) {\n ycov -= clamp(r.x + 0.5, 0.0, 1.0);\n ywgt = max(ywgt, clamp(1.0 - abs(r.x) * 2.0, 0.0, 1.0));\n }\n\n if (code > 1u) {\n ycov += clamp(r.y + 0.5, 0.0, 1.0);\n ywgt = max(ywgt, clamp(1.0 - abs(r.y) * 2.0, 0.0, 1.0));\n }\n }\n }\n\n return CalcCoverage(xcov, ycov, xwgt, ywgt, glyphData.w);\n}\n\nfloat SlugRender(vec2 renderCoord, vec4 bandTransform, ivec4 glyphData) {\n vec2 emsPerPixel = fwidth(renderCoord);\n\n#if defined(SLUG_ADAPTIVE_SUPERSAMPLE)\n // Per-pixel rotated RGSS-4. The base RGSS offsets are rotated by a\n // unique angle per fragment (interleaved gradient noise). This converts\n // structured aliasing shimmer into uncorrelated grain that the eye\n // naturally filters out, much closer to how hardware MSAA on many small\n // triangles behaves perceptually.\n float noise = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715))));\n float angle = noise * 6.2831853;\n float ca = cos(angle), sa = sin(angle);\n\n // Base RGSS offsets rotated by per-pixel angle\n vec2 o0 = vec2(ca * -0.375 - sa * 0.125, sa * -0.375 + ca * 0.125) * emsPerPixel;\n vec2 o1 = vec2(ca * 0.125 - sa * 0.375, sa * 0.125 + ca * 0.375) * emsPerPixel;\n vec2 o2 = vec2(ca * 0.375 - sa * -0.125, sa * 0.375 + ca * -0.125) * emsPerPixel;\n vec2 o3 = vec2(ca * -0.125 - sa * -0.375, sa * -0.125 + ca * -0.375) * emsPerPixel;\n\n float coverage =\n SlugRenderSingle(renderCoord + o0, emsPerPixel, bandTransform, glyphData) +\n SlugRenderSingle(renderCoord + o1, emsPerPixel, bandTransform, glyphData) +\n SlugRenderSingle(renderCoord + o2, emsPerPixel, bandTransform, glyphData) +\n SlugRenderSingle(renderCoord + o3, emsPerPixel, bandTransform, glyphData);\n return coverage * 0.25;\n#else\n return SlugRenderSingle(renderCoord, emsPerPixel, bandTransform, glyphData);\n#endif\n}\n\nvoid main() {\n float coverage = SlugRender(v_texcoord, v_banding, v_glyph);\n outColor = v_color * coverage;\n}\n";
6
6
 
7
- // Slug shader source re-exports
8
- // The .glsl/.wgsl files are the single source of truth, imported as strings
9
- // at build time via the glslPlugin in rollup.config.js
10
7
  // @ts-ignore - resolved by rollup glslPlugin
11
8
  const vertexShaderGLSL300 = vertGLSL;
12
9
  const fragmentShaderGLSL300 = fragGLSL;
@@ -1,10 +1,7 @@
1
- var vertGLSL = "#version 300 es\n// GLSL 300 es port of the Slug vertex shader\n// Eric Lengyel, MIT License, 2017\n\nprecision highp float;\nprecision highp int;\n\n// Per-vertex attributes (5 x vec4, matching Slug reference layout)\nlayout(location = 0) in vec4 a_pos; // .xy = object-space position, .zw = outward normal\nlayout(location = 1) in vec4 a_tex; // .xy = em-space sample coords, .z = packed glyph loc, .w = packed band max + flags\nlayout(location = 2) in vec4 a_jac; // inverse Jacobian (2x2): (j00, j01, j10, j11)\nlayout(location = 3) in vec4 a_bnd; // (bandScaleX, bandScaleY, bandOffsetX, bandOffsetY)\nlayout(location = 4) in vec4 a_col; // vertex color RGBA\n\nuniform mat4 slug_matrix; // MVP matrix (rows as vec4s)\nuniform vec2 slug_viewport; // viewport dimensions in pixels\n\nout vec4 v_color;\nout vec2 v_texcoord;\nflat out vec4 v_banding;\nflat out ivec4 v_glyph;\n\nvoid SlugUnpack(vec4 tex, vec4 bnd, out vec4 vbnd, out ivec4 vgly) {\n uvec2 g = floatBitsToUint(tex.zw);\n vgly = ivec4(g.x & 0xFFFFu, g.x >> 16u, g.y & 0xFFFFu, g.y >> 16u);\n vbnd = bnd;\n}\n\nvec2 SlugDilate(vec4 pos, vec4 tex, vec4 jac, vec4 m0, vec4 m1, vec4 m3, vec2 dim, out vec2 vpos) {\n vec2 n = normalize(pos.zw);\n float s = dot(m3.xy, pos.xy) + m3.w;\n float t = dot(m3.xy, n);\n\n float u = (s * dot(m0.xy, n) - t * (dot(m0.xy, pos.xy) + m0.w)) * dim.x;\n float v = (s * dot(m1.xy, n) - t * (dot(m1.xy, pos.xy) + m1.w)) * dim.y;\n\n float s2 = s * s;\n float st = s * t;\n float uv = u * u + v * v;\n vec2 d = pos.zw * (s2 * (st + sqrt(uv)) / (uv - st * st));\n\n vpos = pos.xy + d;\n return vec2(tex.x + dot(d, jac.xy), tex.y + dot(d, jac.zw));\n}\n\nvoid main() {\n vec2 p;\n\n // Dynamic dilation: expand quad by a pixel to prevent edge clipping.\n v_texcoord = SlugDilate(a_pos, a_tex, a_jac,\n slug_matrix[0], slug_matrix[1], slug_matrix[3],\n slug_viewport, p);\n\n // MVP transform on dilated position.\n gl_Position.x = p.x * slug_matrix[0].x + p.y * slug_matrix[0].y + slug_matrix[0].w;\n gl_Position.y = p.x * slug_matrix[1].x + p.y * slug_matrix[1].y + slug_matrix[1].w;\n gl_Position.z = p.x * slug_matrix[2].x + p.y * slug_matrix[2].y + slug_matrix[2].w;\n gl_Position.w = p.x * slug_matrix[3].x + p.y * slug_matrix[3].y + slug_matrix[3].w;\n\n SlugUnpack(a_tex, a_bnd, v_banding, v_glyph);\n v_color = a_col;\n}\n";
1
+ var vertGLSL = "#version 300 es\n// GLSL 300 es port of the Slug vertex shader\n// Eric Lengyel, MIT License, 2017\n\nprecision highp float;\nprecision highp int;\n\n// Per-vertex attributes (5 x vec4, matching Slug reference layout)\nlayout(location = 0) in vec4 a_pos; // .xy = object-space position, .zw = outward normal\nlayout(location = 1) in vec4 a_tex; // .xy = em-space sample coords, .z = packed glyph loc, .w = packed band max + flags\nlayout(location = 2) in vec4 a_jac; // inverse Jacobian (2x2): (j00, j01, j10, j11)\nlayout(location = 3) in vec4 a_bnd; // (bandScaleX, bandScaleY, bandOffsetX, bandOffsetY)\nlayout(location = 4) in vec4 a_col; // vertex color RGBA\n\nuniform mat4 slug_matrix; // MVP matrix (rows as vec4s)\nuniform vec2 slug_viewport; // viewport dimensions in pixels\n\nout vec4 v_color;\nout vec2 v_texcoord;\nflat out vec4 v_banding;\nflat out ivec4 v_glyph;\n\nvoid SlugUnpack(vec4 tex, vec4 bnd, out vec4 vbnd, out ivec4 vgly) {\n uvec2 g = floatBitsToUint(tex.zw);\n vgly = ivec4(g.x & 0xFFFFu, g.x >> 16u, g.y & 0xFFFFu, g.y >> 16u);\n vbnd = bnd;\n}\n\nvec2 SlugDilate(vec4 pos, vec4 tex, vec4 jac, vec4 m0, vec4 m1, vec4 m3, vec2 dim, out vec2 vpos) {\n vec2 n = normalize(pos.zw);\n float s = dot(m3.xy, pos.xy) + m3.w;\n float t = dot(m3.xy, n);\n\n float u = (s * dot(m0.xy, n) - t * (dot(m0.xy, pos.xy) + m0.w)) * dim.x;\n float v = (s * dot(m1.xy, n) - t * (dot(m1.xy, pos.xy) + m1.w)) * dim.y;\n\n float s2 = s * s;\n float st = s * t;\n float uv = u * u + v * v;\n vec2 d = pos.zw * (s2 * (st + sqrt(uv)) / (uv - st * st));\n\n vpos = pos.xy + d;\n return vec2(tex.x + dot(d, jac.xy), tex.y + dot(d, jac.zw));\n}\n\nvoid main() {\n vec2 p;\n\n // Dynamic dilation: expand quad by a pixel to prevent edge clipping\n v_texcoord = SlugDilate(a_pos, a_tex, a_jac,\n slug_matrix[0], slug_matrix[1], slug_matrix[3],\n slug_viewport, p);\n\n // MVP transform on dilated position\n gl_Position.x = p.x * slug_matrix[0].x + p.y * slug_matrix[0].y + slug_matrix[0].w;\n gl_Position.y = p.x * slug_matrix[1].x + p.y * slug_matrix[1].y + slug_matrix[1].w;\n gl_Position.z = p.x * slug_matrix[2].x + p.y * slug_matrix[2].y + slug_matrix[2].w;\n gl_Position.w = p.x * slug_matrix[3].x + p.y * slug_matrix[3].y + slug_matrix[3].w;\n\n SlugUnpack(a_tex, a_bnd, v_banding, v_glyph);\n v_color = a_col;\n}\n";
2
2
 
3
3
  var fragGLSL = "#version 300 es\n// GLSL 300 es port of the Slug fragment shader\n// Eric Lengyel, MIT License, 2017\n\nprecision highp float;\nprecision highp int;\n\n#define kLogBandTextureWidth 12\n\nin vec4 v_color;\nin vec2 v_texcoord;\nflat in vec4 v_banding;\nflat in ivec4 v_glyph;\n\nuniform sampler2D curveTexture; // RGBA32F control points\nuniform highp usampler2D bandTexture; // RGBA32UI band data\n\nlayout(location = 0) out vec4 outColor;\n\nuint CalcRootCode(float y1, float y2, float y3) {\n uint i1 = floatBitsToUint(y1) >> 31u;\n uint i2 = floatBitsToUint(y2) >> 30u;\n uint i3 = floatBitsToUint(y3) >> 29u;\n\n uint shift = (i2 & 2u) | (i1 & ~2u);\n shift = (i3 & 4u) | (shift & ~4u);\n\n return (0x2E74u >> shift) & 0x0101u;\n}\n\nvec2 SolveHorizPoly(vec4 p12, vec2 p3) {\n vec2 a = p12.xy - p12.zw * 2.0 + p3;\n vec2 b = p12.xy - p12.zw;\n float ra = 1.0 / a.y;\n float rb = 0.5 / b.y;\n\n float d = sqrt(max(b.y * b.y - a.y * p12.y, 0.0));\n float t1 = (b.y - d) * ra;\n float t2 = (b.y + d) * ra;\n\n if (abs(a.y) < 1.0 / 65536.0) t1 = t2 = p12.y * rb;\n\n return vec2((a.x * t1 - b.x * 2.0) * t1 + p12.x, (a.x * t2 - b.x * 2.0) * t2 + p12.x);\n}\n\nvec2 SolveVertPoly(vec4 p12, vec2 p3) {\n vec2 a = p12.xy - p12.zw * 2.0 + p3;\n vec2 b = p12.xy - p12.zw;\n float ra = 1.0 / a.x;\n float rb = 0.5 / b.x;\n\n float d = sqrt(max(b.x * b.x - a.x * p12.x, 0.0));\n float t1 = (b.x - d) * ra;\n float t2 = (b.x + d) * ra;\n\n if (abs(a.x) < 1.0 / 65536.0) t1 = t2 = p12.x * rb;\n\n return vec2((a.y * t1 - b.y * 2.0) * t1 + p12.y, (a.y * t2 - b.y * 2.0) * t2 + p12.y);\n}\n\nivec2 CalcBandLoc(ivec2 glyphLoc, uint offset) {\n ivec2 bandLoc = ivec2(glyphLoc.x + int(offset), glyphLoc.y);\n bandLoc.y += bandLoc.x >> kLogBandTextureWidth;\n bandLoc.x &= (1 << kLogBandTextureWidth) - 1;\n return bandLoc;\n}\n\nfloat CalcCoverage(float xcov, float ycov, float xwgt, float ywgt, int flags) {\n float coverage = max(abs(xcov * xwgt + ycov * ywgt) / max(xwgt + ywgt, 1.0 / 65536.0), min(abs(xcov), abs(ycov)));\n\n#if defined(SLUG_EVENODD)\n if ((flags & 0x1000) == 0) {\n#endif\n coverage = clamp(coverage, 0.0, 1.0);\n#if defined(SLUG_EVENODD)\n } else {\n coverage = 1.0 - abs(1.0 - fract(coverage * 0.5) * 2.0);\n }\n#endif\n\n#if defined(SLUG_WEIGHT)\n coverage = sqrt(coverage);\n#endif\n\n return coverage;\n}\n\nfloat SlugRenderSingle(vec2 renderCoord, vec2 emsPerPixel, vec4 bandTransform, ivec4 glyphData) {\n int curveIndex;\n\n vec2 pixelsPerEm = 1.0 / emsPerPixel;\n\n ivec2 bandMax = glyphData.zw;\n bandMax.y &= 0x00FF;\n\n ivec2 bandIndex = clamp(ivec2(renderCoord * bandTransform.xy + bandTransform.zw), ivec2(0, 0), bandMax);\n ivec2 glyphLoc = glyphData.xy;\n\n float xcov = 0.0;\n float xwgt = 0.0;\n\n uvec2 hbandData = texelFetch(bandTexture, ivec2(glyphLoc.x + bandIndex.y, glyphLoc.y), 0).xy;\n ivec2 hbandLoc = CalcBandLoc(glyphLoc, hbandData.y);\n\n for (curveIndex = 0; curveIndex < int(hbandData.x); curveIndex++) {\n ivec2 curveLoc = ivec2(texelFetch(bandTexture, ivec2(hbandLoc.x + curveIndex, hbandLoc.y), 0).xy);\n\n vec4 p12 = texelFetch(curveTexture, curveLoc, 0) - vec4(renderCoord, renderCoord);\n vec2 p3 = texelFetch(curveTexture, ivec2(curveLoc.x + 1, curveLoc.y), 0).xy - renderCoord;\n\n if (max(max(p12.x, p12.z), p3.x) * pixelsPerEm.x < -0.5) break;\n\n uint code = CalcRootCode(p12.y, p12.w, p3.y);\n if (code != 0u) {\n vec2 r = SolveHorizPoly(p12, p3) * pixelsPerEm.x;\n\n if ((code & 1u) != 0u) {\n xcov += clamp(r.x + 0.5, 0.0, 1.0);\n xwgt = max(xwgt, clamp(1.0 - abs(r.x) * 2.0, 0.0, 1.0));\n }\n\n if (code > 1u) {\n xcov -= clamp(r.y + 0.5, 0.0, 1.0);\n xwgt = max(xwgt, clamp(1.0 - abs(r.y) * 2.0, 0.0, 1.0));\n }\n }\n }\n\n float ycov = 0.0;\n float ywgt = 0.0;\n\n uvec2 vbandData = texelFetch(bandTexture, ivec2(glyphLoc.x + bandMax.y + 1 + bandIndex.x, glyphLoc.y), 0).xy;\n ivec2 vbandLoc = CalcBandLoc(glyphLoc, vbandData.y);\n\n for (curveIndex = 0; curveIndex < int(vbandData.x); curveIndex++) {\n ivec2 curveLoc = ivec2(texelFetch(bandTexture, ivec2(vbandLoc.x + curveIndex, vbandLoc.y), 0).xy);\n vec4 p12 = texelFetch(curveTexture, curveLoc, 0) - vec4(renderCoord, renderCoord);\n vec2 p3 = texelFetch(curveTexture, ivec2(curveLoc.x + 1, curveLoc.y), 0).xy - renderCoord;\n\n if (max(max(p12.y, p12.w), p3.y) * pixelsPerEm.y < -0.5) break;\n\n uint code = CalcRootCode(p12.x, p12.z, p3.x);\n if (code != 0u) {\n vec2 r = SolveVertPoly(p12, p3) * pixelsPerEm.y;\n\n if ((code & 1u) != 0u) {\n ycov -= clamp(r.x + 0.5, 0.0, 1.0);\n ywgt = max(ywgt, clamp(1.0 - abs(r.x) * 2.0, 0.0, 1.0));\n }\n\n if (code > 1u) {\n ycov += clamp(r.y + 0.5, 0.0, 1.0);\n ywgt = max(ywgt, clamp(1.0 - abs(r.y) * 2.0, 0.0, 1.0));\n }\n }\n }\n\n return CalcCoverage(xcov, ycov, xwgt, ywgt, glyphData.w);\n}\n\nfloat SlugRender(vec2 renderCoord, vec4 bandTransform, ivec4 glyphData) {\n vec2 emsPerPixel = fwidth(renderCoord);\n\n#if defined(SLUG_ADAPTIVE_SUPERSAMPLE)\n // Per-pixel rotated RGSS-4. The base RGSS offsets are rotated by a\n // unique angle per fragment (interleaved gradient noise). This converts\n // structured aliasing shimmer into uncorrelated grain that the eye\n // naturally filters out, much closer to how hardware MSAA on many small\n // triangles behaves perceptually.\n float noise = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715))));\n float angle = noise * 6.2831853;\n float ca = cos(angle), sa = sin(angle);\n\n // Base RGSS offsets rotated by per-pixel angle\n vec2 o0 = vec2(ca * -0.375 - sa * 0.125, sa * -0.375 + ca * 0.125) * emsPerPixel;\n vec2 o1 = vec2(ca * 0.125 - sa * 0.375, sa * 0.125 + ca * 0.375) * emsPerPixel;\n vec2 o2 = vec2(ca * 0.375 - sa * -0.125, sa * 0.375 + ca * -0.125) * emsPerPixel;\n vec2 o3 = vec2(ca * -0.125 - sa * -0.375, sa * -0.125 + ca * -0.375) * emsPerPixel;\n\n float coverage =\n SlugRenderSingle(renderCoord + o0, emsPerPixel, bandTransform, glyphData) +\n SlugRenderSingle(renderCoord + o1, emsPerPixel, bandTransform, glyphData) +\n SlugRenderSingle(renderCoord + o2, emsPerPixel, bandTransform, glyphData) +\n SlugRenderSingle(renderCoord + o3, emsPerPixel, bandTransform, glyphData);\n return coverage * 0.25;\n#else\n return SlugRenderSingle(renderCoord, emsPerPixel, bandTransform, glyphData);\n#endif\n}\n\nvoid main() {\n float coverage = SlugRender(v_texcoord, v_banding, v_glyph);\n outColor = v_color * coverage;\n}\n";
4
4
 
5
- // Slug shader source re-exports
6
- // The .glsl/.wgsl files are the single source of truth, imported as strings
7
- // at build time via the glslPlugin in rollup.config.js
8
5
  // @ts-ignore - resolved by rollup glslPlugin
9
6
  const vertexShaderGLSL300 = vertGLSL;
10
7
  const fragmentShaderGLSL300 = fragGLSL;
@@ -4,9 +4,6 @@ var vertWGSL = "// WGSL port of the reference Slug vertex shader\n// Original HL
4
4
 
5
5
  var fragWGSL = "// WGSL port of the reference Slug pixel shader\n// by Eric Lengyel, MIT License, Copyright 2017\n\nconst kLogBandTextureWidth: u32 = 12u;\n\n@group(0) @binding(1) var curveTexture: texture_2d<f32>;\n@group(0) @binding(2) var bandTexture: texture_2d<u32>;\n\nstruct FragmentInput {\n @location(0) color: vec4<f32>,\n @location(1) texcoord: vec2<f32>,\n @location(2) @interpolate(flat) banding: vec4<f32>,\n @location(3) @interpolate(flat) glyph: vec4<i32>,\n};\n\nfn CalcRootCode(y1: f32, y2: f32, y3: f32) -> u32 {\n let i1 = bitcast<u32>(y1) >> 31u;\n let i2 = bitcast<u32>(y2) >> 30u;\n let i3 = bitcast<u32>(y3) >> 29u;\n\n var shift = (i2 & 2u) | (i1 & ~2u);\n shift = (i3 & 4u) | (shift & ~4u);\n\n return (0x2E74u >> shift) & 0x0101u;\n}\n\nfn SolveHorizPoly(p12: vec4<f32>, p3: vec2<f32>) -> vec2<f32> {\n let a = p12.xy - p12.zw * 2.0 + p3;\n let b = p12.xy - p12.zw;\n let ra = 1.0 / a.y;\n let rb = 0.5 / b.y;\n\n let d = sqrt(max(b.y * b.y - a.y * p12.y, 0.0));\n var t1 = (b.y - d) * ra;\n var t2 = (b.y + d) * ra;\n\n if (abs(a.y) < 1.0 / 65536.0) {\n t1 = p12.y * rb;\n t2 = t1;\n }\n\n return vec2<f32>(\n (a.x * t1 - b.x * 2.0) * t1 + p12.x,\n (a.x * t2 - b.x * 2.0) * t2 + p12.x\n );\n}\n\nfn SolveVertPoly(p12: vec4<f32>, p3: vec2<f32>) -> vec2<f32> {\n let a = p12.xy - p12.zw * 2.0 + p3;\n let b = p12.xy - p12.zw;\n let ra = 1.0 / a.x;\n let rb = 0.5 / b.x;\n\n let d = sqrt(max(b.x * b.x - a.x * p12.x, 0.0));\n var t1 = (b.x - d) * ra;\n var t2 = (b.x + d) * ra;\n\n if (abs(a.x) < 1.0 / 65536.0) {\n t1 = p12.x * rb;\n t2 = t1;\n }\n\n return vec2<f32>(\n (a.y * t1 - b.y * 2.0) * t1 + p12.y,\n (a.y * t2 - b.y * 2.0) * t2 + p12.y\n );\n}\n\nfn CalcBandLoc(glyphLoc: vec2<i32>, offset: u32) -> vec2<i32> {\n var bandLoc = vec2<i32>(glyphLoc.x + i32(offset), glyphLoc.y);\n let bandShift = kLogBandTextureWidth;\n let bandMask = i32((1u << bandShift) - 1u);\n bandLoc.y += bandLoc.x >> bandShift;\n bandLoc.x &= bandMask;\n return bandLoc;\n}\n\nfn CalcCoverage(xcov: f32, ycov: f32, xwgt: f32, ywgt: f32) -> f32 {\n var coverage = max(\n abs(xcov * xwgt + ycov * ywgt) / max(xwgt + ywgt, 1.0 / 65536.0),\n min(abs(xcov), abs(ycov))\n );\n coverage = clamp(coverage, 0.0, 1.0);\n return coverage;\n}\n\nfn SlugRender(renderCoord: vec2<f32>, bandTransform: vec4<f32>, glyphData: vec4<i32>) -> f32 {\n let emsPerPixel = fwidth(renderCoord);\n let pixelsPerEm = 1.0 / emsPerPixel;\n\n var bandMax = glyphData.zw;\n bandMax.y &= 0x00FF;\n\n let bandIndex = clamp(\n vec2<i32>(renderCoord * bandTransform.xy + bandTransform.zw),\n vec2<i32>(0, 0),\n bandMax\n );\n let glyphLoc = glyphData.xy;\n\n var xcov = 0.0;\n var xwgt = 0.0;\n\n let hbandData = textureLoad(bandTexture, vec2<i32>(glyphLoc.x + bandIndex.y, glyphLoc.y), 0).xy;\n let hbandLoc = CalcBandLoc(glyphLoc, hbandData.y);\n\n for (var ci = 0; ci < i32(hbandData.x); ci++) {\n let curveLoc = vec2<i32>(textureLoad(bandTexture, vec2<i32>(hbandLoc.x + ci, hbandLoc.y), 0).xy);\n\n let p12 = textureLoad(curveTexture, curveLoc, 0) - vec4<f32>(renderCoord, renderCoord);\n let p3 = textureLoad(curveTexture, vec2<i32>(curveLoc.x + 1, curveLoc.y), 0).xy - renderCoord;\n\n if (max(max(p12.x, p12.z), p3.x) * pixelsPerEm.x < -0.5) { break; }\n\n let code = CalcRootCode(p12.y, p12.w, p3.y);\n if (code != 0u) {\n let r = SolveHorizPoly(p12, p3) * pixelsPerEm.x;\n\n if ((code & 1u) != 0u) {\n xcov += clamp(r.x + 0.5, 0.0, 1.0);\n xwgt = max(xwgt, clamp(1.0 - abs(r.x) * 2.0, 0.0, 1.0));\n }\n if (code > 1u) {\n xcov -= clamp(r.y + 0.5, 0.0, 1.0);\n xwgt = max(xwgt, clamp(1.0 - abs(r.y) * 2.0, 0.0, 1.0));\n }\n }\n }\n\n var ycov = 0.0;\n var ywgt = 0.0;\n\n let vbandData = textureLoad(bandTexture, vec2<i32>(glyphLoc.x + bandMax.y + 1 + bandIndex.x, glyphLoc.y), 0).xy;\n let vbandLoc = CalcBandLoc(glyphLoc, vbandData.y);\n\n for (var ci = 0; ci < i32(vbandData.x); ci++) {\n let curveLoc = vec2<i32>(textureLoad(bandTexture, vec2<i32>(vbandLoc.x + ci, vbandLoc.y), 0).xy);\n let p12 = textureLoad(curveTexture, curveLoc, 0) - vec4<f32>(renderCoord, renderCoord);\n let p3 = textureLoad(curveTexture, vec2<i32>(curveLoc.x + 1, curveLoc.y), 0).xy - renderCoord;\n\n if (max(max(p12.y, p12.w), p3.y) * pixelsPerEm.y < -0.5) { break; }\n\n let code = CalcRootCode(p12.x, p12.z, p3.x);\n if (code != 0u) {\n let r = SolveVertPoly(p12, p3) * pixelsPerEm.y;\n\n if ((code & 1u) != 0u) {\n ycov -= clamp(r.x + 0.5, 0.0, 1.0);\n ywgt = max(ywgt, clamp(1.0 - abs(r.x) * 2.0, 0.0, 1.0));\n }\n if (code > 1u) {\n ycov += clamp(r.y + 0.5, 0.0, 1.0);\n ywgt = max(ywgt, clamp(1.0 - abs(r.y) * 2.0, 0.0, 1.0));\n }\n }\n }\n\n return CalcCoverage(xcov, ycov, xwgt, ywgt);\n}\n\n@fragment\nfn fs_main(in: FragmentInput) -> @location(0) vec4<f32> {\n let coverage = SlugRender(in.texcoord, in.banding, in.glyph);\n return in.color * coverage;\n}\n";
6
6
 
7
- // Slug shader source re-exports
8
- // The .glsl/.wgsl files are the single source of truth, imported as strings
9
- // at build time via the glslPlugin in rollup.config.js
10
7
  // @ts-ignore - resolved by rollup glslPlugin
11
8
  const vertexShaderWGSL = vertWGSL;
12
9
  const fragmentShaderWGSL = fragWGSL;
@@ -2,9 +2,6 @@ var vertWGSL = "// WGSL port of the reference Slug vertex shader\n// Original HL
2
2
 
3
3
  var fragWGSL = "// WGSL port of the reference Slug pixel shader\n// by Eric Lengyel, MIT License, Copyright 2017\n\nconst kLogBandTextureWidth: u32 = 12u;\n\n@group(0) @binding(1) var curveTexture: texture_2d<f32>;\n@group(0) @binding(2) var bandTexture: texture_2d<u32>;\n\nstruct FragmentInput {\n @location(0) color: vec4<f32>,\n @location(1) texcoord: vec2<f32>,\n @location(2) @interpolate(flat) banding: vec4<f32>,\n @location(3) @interpolate(flat) glyph: vec4<i32>,\n};\n\nfn CalcRootCode(y1: f32, y2: f32, y3: f32) -> u32 {\n let i1 = bitcast<u32>(y1) >> 31u;\n let i2 = bitcast<u32>(y2) >> 30u;\n let i3 = bitcast<u32>(y3) >> 29u;\n\n var shift = (i2 & 2u) | (i1 & ~2u);\n shift = (i3 & 4u) | (shift & ~4u);\n\n return (0x2E74u >> shift) & 0x0101u;\n}\n\nfn SolveHorizPoly(p12: vec4<f32>, p3: vec2<f32>) -> vec2<f32> {\n let a = p12.xy - p12.zw * 2.0 + p3;\n let b = p12.xy - p12.zw;\n let ra = 1.0 / a.y;\n let rb = 0.5 / b.y;\n\n let d = sqrt(max(b.y * b.y - a.y * p12.y, 0.0));\n var t1 = (b.y - d) * ra;\n var t2 = (b.y + d) * ra;\n\n if (abs(a.y) < 1.0 / 65536.0) {\n t1 = p12.y * rb;\n t2 = t1;\n }\n\n return vec2<f32>(\n (a.x * t1 - b.x * 2.0) * t1 + p12.x,\n (a.x * t2 - b.x * 2.0) * t2 + p12.x\n );\n}\n\nfn SolveVertPoly(p12: vec4<f32>, p3: vec2<f32>) -> vec2<f32> {\n let a = p12.xy - p12.zw * 2.0 + p3;\n let b = p12.xy - p12.zw;\n let ra = 1.0 / a.x;\n let rb = 0.5 / b.x;\n\n let d = sqrt(max(b.x * b.x - a.x * p12.x, 0.0));\n var t1 = (b.x - d) * ra;\n var t2 = (b.x + d) * ra;\n\n if (abs(a.x) < 1.0 / 65536.0) {\n t1 = p12.x * rb;\n t2 = t1;\n }\n\n return vec2<f32>(\n (a.y * t1 - b.y * 2.0) * t1 + p12.y,\n (a.y * t2 - b.y * 2.0) * t2 + p12.y\n );\n}\n\nfn CalcBandLoc(glyphLoc: vec2<i32>, offset: u32) -> vec2<i32> {\n var bandLoc = vec2<i32>(glyphLoc.x + i32(offset), glyphLoc.y);\n let bandShift = kLogBandTextureWidth;\n let bandMask = i32((1u << bandShift) - 1u);\n bandLoc.y += bandLoc.x >> bandShift;\n bandLoc.x &= bandMask;\n return bandLoc;\n}\n\nfn CalcCoverage(xcov: f32, ycov: f32, xwgt: f32, ywgt: f32) -> f32 {\n var coverage = max(\n abs(xcov * xwgt + ycov * ywgt) / max(xwgt + ywgt, 1.0 / 65536.0),\n min(abs(xcov), abs(ycov))\n );\n coverage = clamp(coverage, 0.0, 1.0);\n return coverage;\n}\n\nfn SlugRender(renderCoord: vec2<f32>, bandTransform: vec4<f32>, glyphData: vec4<i32>) -> f32 {\n let emsPerPixel = fwidth(renderCoord);\n let pixelsPerEm = 1.0 / emsPerPixel;\n\n var bandMax = glyphData.zw;\n bandMax.y &= 0x00FF;\n\n let bandIndex = clamp(\n vec2<i32>(renderCoord * bandTransform.xy + bandTransform.zw),\n vec2<i32>(0, 0),\n bandMax\n );\n let glyphLoc = glyphData.xy;\n\n var xcov = 0.0;\n var xwgt = 0.0;\n\n let hbandData = textureLoad(bandTexture, vec2<i32>(glyphLoc.x + bandIndex.y, glyphLoc.y), 0).xy;\n let hbandLoc = CalcBandLoc(glyphLoc, hbandData.y);\n\n for (var ci = 0; ci < i32(hbandData.x); ci++) {\n let curveLoc = vec2<i32>(textureLoad(bandTexture, vec2<i32>(hbandLoc.x + ci, hbandLoc.y), 0).xy);\n\n let p12 = textureLoad(curveTexture, curveLoc, 0) - vec4<f32>(renderCoord, renderCoord);\n let p3 = textureLoad(curveTexture, vec2<i32>(curveLoc.x + 1, curveLoc.y), 0).xy - renderCoord;\n\n if (max(max(p12.x, p12.z), p3.x) * pixelsPerEm.x < -0.5) { break; }\n\n let code = CalcRootCode(p12.y, p12.w, p3.y);\n if (code != 0u) {\n let r = SolveHorizPoly(p12, p3) * pixelsPerEm.x;\n\n if ((code & 1u) != 0u) {\n xcov += clamp(r.x + 0.5, 0.0, 1.0);\n xwgt = max(xwgt, clamp(1.0 - abs(r.x) * 2.0, 0.0, 1.0));\n }\n if (code > 1u) {\n xcov -= clamp(r.y + 0.5, 0.0, 1.0);\n xwgt = max(xwgt, clamp(1.0 - abs(r.y) * 2.0, 0.0, 1.0));\n }\n }\n }\n\n var ycov = 0.0;\n var ywgt = 0.0;\n\n let vbandData = textureLoad(bandTexture, vec2<i32>(glyphLoc.x + bandMax.y + 1 + bandIndex.x, glyphLoc.y), 0).xy;\n let vbandLoc = CalcBandLoc(glyphLoc, vbandData.y);\n\n for (var ci = 0; ci < i32(vbandData.x); ci++) {\n let curveLoc = vec2<i32>(textureLoad(bandTexture, vec2<i32>(vbandLoc.x + ci, vbandLoc.y), 0).xy);\n let p12 = textureLoad(curveTexture, curveLoc, 0) - vec4<f32>(renderCoord, renderCoord);\n let p3 = textureLoad(curveTexture, vec2<i32>(curveLoc.x + 1, curveLoc.y), 0).xy - renderCoord;\n\n if (max(max(p12.y, p12.w), p3.y) * pixelsPerEm.y < -0.5) { break; }\n\n let code = CalcRootCode(p12.x, p12.z, p3.x);\n if (code != 0u) {\n let r = SolveVertPoly(p12, p3) * pixelsPerEm.y;\n\n if ((code & 1u) != 0u) {\n ycov -= clamp(r.x + 0.5, 0.0, 1.0);\n ywgt = max(ywgt, clamp(1.0 - abs(r.x) * 2.0, 0.0, 1.0));\n }\n if (code > 1u) {\n ycov += clamp(r.y + 0.5, 0.0, 1.0);\n ywgt = max(ywgt, clamp(1.0 - abs(r.y) * 2.0, 0.0, 1.0));\n }\n }\n }\n\n return CalcCoverage(xcov, ycov, xwgt, ywgt);\n}\n\n@fragment\nfn fs_main(in: FragmentInput) -> @location(0) vec4<f32> {\n let coverage = SlugRender(in.texcoord, in.banding, in.glyph);\n return in.color * coverage;\n}\n";
4
4
 
5
- // Slug shader source re-exports
6
- // The .glsl/.wgsl files are the single source of truth, imported as strings
7
- // at build time via the glslPlugin in rollup.config.js
8
5
  // @ts-ignore - resolved by rollup glslPlugin
9
6
  const vertexShaderWGSL = vertWGSL;
10
7
  const fragmentShaderWGSL = fragWGSL;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "three-text",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "3D font rendering and text layout engine for the web",
5
5
  "main": "dist/three/index.cjs",
6
6
  "module": "dist/three/index.js",