three-text 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/LICENSE_THIRD_PARTY +15 -0
  2. package/README.md +80 -50
  3. package/dist/index.cjs +66 -20
  4. package/dist/index.d.ts +8 -0
  5. package/dist/index.js +66 -20
  6. package/dist/index.min.cjs +310 -307
  7. package/dist/index.min.js +265 -262
  8. package/dist/index.umd.js +68 -21
  9. package/dist/index.umd.min.js +268 -265
  10. package/dist/three/index.cjs +2 -1
  11. package/dist/three/index.d.ts +1 -0
  12. package/dist/three/index.js +2 -1
  13. package/dist/three/react.cjs +35 -17
  14. package/dist/three/react.d.ts +8 -0
  15. package/dist/three/react.js +35 -17
  16. package/dist/types/core/Text.d.ts +6 -0
  17. package/dist/types/core/types.d.ts +2 -33
  18. package/dist/types/three/index.d.ts +1 -0
  19. package/dist/types/vector/core/index.d.ts +28 -0
  20. package/dist/types/vector/index.d.ts +17 -12
  21. package/dist/types/vector/react.d.ts +3 -4
  22. package/dist/types/vector/slug/SlugPacker.d.ts +2 -0
  23. package/dist/types/vector/slug/curveUtils.d.ts +6 -0
  24. package/dist/types/vector/slug/index.d.ts +8 -0
  25. package/dist/types/vector/slug/shaderStrings.d.ts +4 -0
  26. package/dist/types/vector/slug/slugGLSL.d.ts +21 -0
  27. package/dist/types/vector/slug/slugTSL.d.ts +13 -0
  28. package/dist/types/vector/slug/types.d.ts +30 -0
  29. package/dist/types/vector/slug/unpackVertices.d.ts +11 -0
  30. package/dist/types/vector/webgl/index.d.ts +7 -3
  31. package/dist/types/vector/webgpu/index.d.ts +4 -4
  32. package/dist/vector/all.cjs +21 -0
  33. package/dist/vector/all.d.ts +134 -0
  34. package/dist/vector/all.js +2 -0
  35. package/dist/vector/core/index.cjs +856 -0
  36. package/dist/vector/core/index.d.ts +63 -0
  37. package/dist/vector/core/index.js +854 -0
  38. package/dist/vector/core.cjs +5489 -0
  39. package/dist/vector/core.d.ts +402 -0
  40. package/dist/vector/core.js +5486 -0
  41. package/dist/vector/index.cjs +5 -1305
  42. package/dist/vector/index.d.ts +41 -67
  43. package/dist/vector/index.js +3 -1306
  44. package/dist/vector/index2.cjs +287 -0
  45. package/dist/vector/index2.js +264 -0
  46. package/dist/vector/loopBlinnTSL.d.ts +69 -0
  47. package/dist/vector/react.cjs +54 -40
  48. package/dist/vector/react.d.ts +11 -2
  49. package/dist/vector/react.js +55 -41
  50. package/dist/vector/slugTSL.cjs +252 -0
  51. package/dist/vector/slugTSL.js +231 -0
  52. package/dist/vector/webgl/index.cjs +131 -201
  53. package/dist/vector/webgl/index.d.ts +19 -44
  54. package/dist/vector/webgl/index.js +131 -201
  55. package/dist/vector/webgpu/index.cjs +100 -283
  56. package/dist/vector/webgpu/index.d.ts +16 -45
  57. package/dist/vector/webgpu/index.js +100 -283
  58. package/package.json +6 -1
  59. package/dist/types/vector/GlyphVectorGeometryBuilder.d.ts +0 -26
  60. package/dist/types/vector/LoopBlinnGeometry.d.ts +0 -68
  61. package/dist/types/vector/loopBlinnTSL.d.ts +0 -11
@@ -107,6 +107,21 @@ complete source and licensing details: https://github.com/hyphenation/tex-hyphen
107
107
 
108
108
  ---
109
109
 
110
+ 4. Slug Library
111
+
112
+ This software includes a port of the Slug library
113
+ (https://github.com/EricLengyel/Slug), a GPU-based vector graphics
114
+ rendering algorithm for text and shapes by Eric Lengyel. The original
115
+ GLSL shaders were ported to Three.js TSL (Three.js Shading Language)
116
+ with added adaptive supersampling
117
+
118
+ Copyright (c) 2017 Eric Lengyel
119
+
120
+ Slug is distributed under the MIT License.
121
+ (See Appendix A for MIT License text)
122
+
123
+ ---
124
+
110
125
  Appendix A: MIT License text
111
126
 
112
127
  Permission is hereby granted, free of charge, to any person obtaining a copy
package/README.md CHANGED
@@ -15,11 +15,11 @@ High fidelity 3D text rendering and layout for the web
15
15
  > [!CAUTION]
16
16
  > three-text is in alpha release and the API may break rapidly. This warning will likely last until summer of 2026. If API stability is important to you, consider pinning your version. Community feedback is encouraged; please open an issue if you have any suggestions or feedback, thank you
17
17
 
18
- **three-text** is a 3D font rendering and text layout library for the web. It supports TTF, OTF, WOFF, and WOFF2 font files and uses [TeX](https://en.wikipedia.org/wiki/TeX)-based parameters for layout, with support for CJK and RTL scripts. Two rendering pipelines share the same core: **mesh** (extruded, lit, deformable geometry) and **vector** (resolution-independent Loop-Blinn outlines). Contours and geometries are cached per glyph for low CPU overhead in text-heavy scenes. Variable fonts are supported
18
+ **three-text** is a 3D font rendering and text layout library for the web. It supports TTF, OTF, WOFF, and WOFF2 font files and uses [TeX](https://en.wikipedia.org/wiki/TeX)-based parameters for layout, with support for CJK and RTL scripts. Two rendering pipelines share the same core: **mesh** (extrudable, deformable geometry) and **vector** (resolution-independent GPU outlines). Contours and geometries are cached per glyph for low CPU overhead in text-heavy scenes. Variable fonts are supported
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 [Loop-Blinn](https://www.microsoft.com/en-us/research/wp-content/uploads/2005/01/p1000-loop.pdf) resolution-independent curve rendering with [Kokojima et al.](https://dl.acm.org/doi/10.1145/1179849.1179997) stencil filling
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
23
 
24
24
  ## Table of contents
25
25
 
@@ -71,10 +71,11 @@ three-text has a framework-agnostic core that processes fonts and generates geom
71
71
  - **`three-text/mesh/webgpu`** - WebGPU mesh buffer utility
72
72
  - **`three-text/mesh/p5`** - p5.js adapter
73
73
  - **`three-text/core`** - Framework-agnostic core (returns raw arrays)
74
- - **`three-text/vector`** - Vector rendering (Loop-Blinn and Kokojima stencil fill, resolution-independent)
74
+ - **`three-text/vector`** - Vector rendering (Slug per-fragment curve evaluation), `Text.create()` returns a `THREE.Group`
75
75
  - **`three-text/vector/react`** - React Three Fiber component for vector text
76
- - **`three-text/vector/webgl`** - WebGL vector renderer
77
- - **`three-text/vector/webgpu`** - WebGPU vector renderer
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)
78
79
  - **`three-text/webgl`** - Deprecated, use `three-text/mesh/webgl`
79
80
  - **`three-text/webgpu`** - Deprecated, use `three-text/mesh/webgpu`
80
81
  - **`three-text/p5`** - Deprecated, use `three-text/mesh/p5`
@@ -86,7 +87,7 @@ Most users will just `import { Text } from 'three-text'` for Three.js projects w
86
87
  The library offers two rendering modes that share the same core (HarfBuzz shaping, Knuth-Plass justification, glyph caching):
87
88
 
88
89
  - **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
89
- - **Vector** (`three-text/vector`): resolution-independent rendering on the GPU without tessellation. Use for text that needs to stay sharp at arbitrary zoom
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
90
91
 
91
92
  Both can be used in the same project from separate entry points
92
93
 
@@ -96,7 +97,7 @@ Both can be used in the same project from separate entry points
96
97
 
97
98
  #### Mesh (Three.js)
98
99
 
99
- Extruded `BufferGeometry` light, shade, and deform as a normal mesh:
100
+ Extruded `BufferGeometry` that you can light, shade, and deform like any mesh:
100
101
 
101
102
  ```javascript
102
103
  import { Text } from 'three-text';
@@ -117,32 +118,30 @@ scene.add(mesh);
117
118
 
118
119
  #### Vector (Three.js)
119
120
 
120
- Resolution-independent outlines via Loop-Blinn stencil passes (see [Vector rendering](#vector-rendering)):
121
+ Resolution-independent outlines via per-fragment curve evaluation (see [Vector rendering](#vector-rendering)):
121
122
 
122
123
  ```javascript
123
124
  import { Text } from 'three-text/vector';
124
- import { woff2Decode } from 'woff-lib/woff2/decode';
125
125
 
126
126
  Text.setHarfBuzzPath('/hb/hb.wasm');
127
- Text.enableWoff2(woff2Decode);
128
127
  const result = await Text.create({
129
128
  text: 'Hello Vector',
130
129
  font: '/fonts/Font.woff2',
131
- size: 72
130
+ size: 72,
131
+ color: [1, 1, 1]
132
132
  });
133
-
134
- const vectorData = result.geometryData;
133
+ scene.add(result.group);
135
134
  ```
136
135
 
137
- Use `createVectorMeshes(vectorData)` from `three-text/vector` for TSL / `WebGPURenderer`, or build your own stencil materials (see [Vector rendering](#vector-rendering))
136
+ `Text.create()` returns a `THREE.Group` ready for `scene.add()`. Uses TSL node materials when the renderer supports them (Three.js r170+), otherwise falls back to a GLSL `RawShaderMaterial` that works with any WebGL2 renderer
138
137
 
139
138
  #### Mesh + vector in one scene
140
139
 
141
- Alias one import to avoid the name collision between `Text` components. The entry points share a core (shaping, layout, font cache) so you can mix them freely fonts load once regardless of which entry point requests them:
140
+ Alias one import to avoid the name collision between `Text` components. The entry points share a core (shaping, layout, font cache) so you can mix them freely; fonts load once regardless of which entry point requests them:
142
141
 
143
142
  ```javascript
144
143
  import { Text as MeshText } from 'three-text';
145
- import { Text as VectorText, createVectorMeshes } from 'three-text/vector';
144
+ import { Text as VectorText } from 'three-text/vector';
146
145
 
147
146
  MeshText.setHarfBuzzPath('/hb/hb.wasm');
148
147
 
@@ -156,10 +155,10 @@ scene.add(new THREE.Mesh(heading.geometry, material));
156
155
  const caption = await VectorText.create({
157
156
  text: 'Caption text',
158
157
  font: '/fonts/Font.woff2',
159
- size: 24
158
+ size: 24,
159
+ color: [1, 1, 1]
160
160
  });
161
- const { interiorMesh, curveMesh, fillMesh } = createVectorMeshes(caption.geometryData);
162
- scene.add(interiorMesh, curveMesh, fillMesh);
161
+ scene.add(caption.group);
163
162
  ```
164
163
 
165
164
  #### React Three Fiber — mesh
@@ -184,7 +183,7 @@ function App() {
184
183
 
185
184
  #### React Three Fiber — vector
186
185
 
187
- The vector `Text` builds three internal meshes (interior / curve / fill) with TSL stencil materials. Requires a renderer that supports `MeshBasicNodeMaterial` (Three.js r170+). With WebGPU, pass a `WebGPURenderer` with `stencil: true` and await `init()` (see the [three.js WebGPU examples](https://threejs.org/examples/?q=webgpu)):
186
+ The vector `Text` component creates a single mesh with a TSL node material that evaluates curve coverage per fragment. Requires a renderer that supports `MeshBasicNodeMaterial` (Three.js r170+). With WebGPU, pass a `WebGPURenderer` and await `init()` (see the [three.js WebGPU examples](https://threejs.org/examples/?q=webgpu)):
188
187
 
189
188
  ```jsx
190
189
  import { Canvas } from '@react-three/fiber';
@@ -199,7 +198,7 @@ function App() {
199
198
  gl={async (props) => {
200
199
  const renderer = new THREE.WebGPURenderer({
201
200
  canvas: props.canvas,
202
- stencil: true
201
+ antialias: true
203
202
  });
204
203
  await renderer.init();
205
204
  return renderer;
@@ -231,7 +230,7 @@ function App() {
231
230
  gl={async (props) => {
232
231
  const renderer = new THREE.WebGPURenderer({
233
232
  canvas: props.canvas,
234
- stencil: true
233
+ antialias: true
235
234
  });
236
235
  await renderer.init();
237
236
  return renderer;
@@ -293,12 +292,13 @@ import { woff2Decode } from 'woff-lib/woff2/decode';
293
292
  Text.setHarfBuzzPath('/hb/hb.wasm');
294
293
  Text.enableWoff2(woff2Decode);
295
294
 
296
- const gl = canvas.getContext('webgl2', { antialias: true, stencil: true });
297
- const renderer = createWebGLVectorRenderer(gl);
295
+ const gl = canvas.getContext('webgl2', { antialias: true });
296
+ const renderer = createWebGLVectorRenderer(gl, {
297
+ adaptiveSupersampling: true
298
+ });
298
299
 
299
300
  const result = await Text.create({ text: 'Hello', font: '/fonts/Font.woff2', size: 72 });
300
- const vectorData = result.geometryData;
301
- renderer.setGeometry(vectorData);
301
+ renderer.setGeometry(result.gpuData);
302
302
 
303
303
  // In render loop:
304
304
  renderer.render(mvpMatrix, new Float32Array([1, 1, 1, 1]));
@@ -315,19 +315,52 @@ Text.setHarfBuzzPath('/hb/hb.wasm');
315
315
  Text.enableWoff2(woff2Decode);
316
316
 
317
317
  const renderer = createWebGPUVectorRenderer(device, format, {
318
- depthStencilFormat: 'depth24plus-stencil8',
319
318
  sampleCount: 4
320
319
  });
321
320
 
322
321
  const result = await Text.create({ text: 'Hello', font: '/fonts/Font.woff2', size: 72 });
323
- const vectorData = result.geometryData;
324
- renderer.setGeometry(vectorData);
322
+ renderer.setGeometry(result.gpuData);
325
323
 
326
324
  // In render pass:
327
325
  renderer.render(passEncoder, mvpMatrix, new Float32Array([1, 1, 1, 1]));
328
326
  ```
329
327
 
330
- See `examples/webgl-vector.html` and `examples/webgpu-vector.html` for raw WebGL/WebGPU demos. The main interactive demo (`examples/index.html`) uses `WebGPURenderer` with TSL node materials for both mesh and Loop-Blinn vector paths. A `WebGLRenderer` variant is available at `examples/index-webgl.html`
328
+ **Adaptive supersampling** is available on the GLSL code path (raw WebGL2 renderer, `createSlugGLSLMesh`, and Three.js vector when TSL is unavailable). It uses rotated-grid supersampling (RGSS, 4 samples per pixel) with a per-fragment rotation angle derived from interleaved gradient noise. This converts structured aliasing shimmer into uncorrelated grain that the eye naturally filters out. Pass `adaptiveSupersampling: true` to enable it. The TSL and WebGPU paths do not currently support this option
329
+
330
+ #### GLSL animation injection
331
+
332
+ For custom vertex animations, `createSlugGLSLMesh` gives direct access to the GLSL vertex shader. This is exported from `three-text/vector` and builds a Three.js `Mesh` backed by a `RawShaderMaterial`:
333
+
334
+ ```javascript
335
+ import { Text, createSlugGLSLMesh } from 'three-text/vector';
336
+
337
+ const result = await Text.create({
338
+ text: 'Hello',
339
+ font: '/fonts/Font.woff2',
340
+ size: 72,
341
+ perGlyphAttributes: true
342
+ });
343
+
344
+ const { mesh, uniforms } = createSlugGLSLMesh(result.gpuData, {
345
+ color: { r: 1, g: 1, b: 1 },
346
+ adaptiveSupersampling: true,
347
+ animationDeclarations: `
348
+ uniform float waveAmplitude;
349
+ `,
350
+ animationBody: `
351
+ vec3 outPos = position;
352
+ outPos.y += sin(glyphIndex * 0.5 + time) * waveAmplitude;
353
+ `,
354
+ uniforms: {
355
+ waveAmplitude: { value: 10.0 }
356
+ }
357
+ });
358
+ scene.add(mesh);
359
+ ```
360
+
361
+ The vertex shader provides `position` (vec3), `glyphCenter` (vec3), `glyphIndex` (float), and `time` (float) for use in your animation body. Your code must write a `vec3 outPos` that becomes the final vertex position
362
+
363
+ The main interactive demo (`examples/index.html`) uses `WebGPURenderer` with TSL node materials for both mesh and vector paths. A `WebGLRenderer` variant for mesh mode is available at `examples/index-webgl.html`
331
364
 
332
365
  ### Coordinate systems
333
366
 
@@ -437,8 +470,8 @@ Then navigate to `http://localhost:3000`
437
470
 
438
471
  three-text renders text from real font files (TTF, OTF, WOFF, WOFF2) with two pipelines:
439
472
 
440
- - **Mesh** tessellated 3D geometry that can be extruded, lit, and shaded like any model
441
- - **Vector** resolution-independent outlines rendered directly from curve data on the GPU, sharp at any zoom or angle
473
+ - **Mesh**: tessellated 3D geometry that can be extruded, lit, and shaded like any model
474
+ - **Vector**: resolution-independent outlines rendered directly from curve data on the GPU, sharp at any zoom or angle
442
475
 
443
476
  Both share the same layout engine (HarfBuzz shaping, Knuth-Plass line breaking) and glyph cache, so a paragraph of 1000 words might only require 50 unique glyphs to be processed
444
477
 
@@ -450,9 +483,9 @@ Existing solutions take different approaches:
450
483
 
451
484
  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
452
485
 
453
- ### Why Loop-Blinn
486
+ ### Why Slug
454
487
 
455
- The vector path uses Loop-Blinn curve rendering, where each quadratic Bezier segment becomes a triangle whose fragment shader evaluates the implicit equation `u² - v = 0` to resolve inside/outside analytically, while interior regions are filled separately. We also evaluated Eric Lengyel's [Slug](https://github.com/EricLengyel/Slug) algorithm, which renders glyphs by casting rays against banded curve data over a dilated bounding polygon, and tested several antialiasing configurations including adaptive supersampling and alpha-to-coverage. Loop-Blinn produced cleaner results under strong perspective and oblique viewing angles, so that is what we use
488
+ 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
456
489
 
457
490
  ## Library structure
458
491
 
@@ -476,14 +509,12 @@ three-text/
476
509
  │ │ ├── index.ts # BufferGeometry wrapper
477
510
  │ │ ├── react.tsx # React component export
478
511
  │ │ └── ThreeText.tsx # React Three Fiber component
479
- │ ├── vector/ # Vector rendering (Loop-Blinn)
480
- │ │ ├── index.ts # Vector entry point and TSL re-exports
481
- │ │ ├── loopBlinnTSL.ts # TSL adapter for Three.js WebGPURenderer
482
- │ │ ├── LoopBlinnGeometry.ts # Fan triangulation + curve extraction
483
- │ │ ├── GlyphVectorGeometryBuilder.ts # Outline collection and geometry packing
484
- │ │ ├── GlyphOutlineCollector.ts # Collects draw callbacks for vector path
485
- │ │ ├── webgl/ # WebGL2 stencil-based renderer
486
- │ │ └── webgpu/ # WebGPU stencil-based renderer
512
+ │ ├── vector/ # Vector rendering (Slug)
513
+ │ │ ├── index.ts # Main entry point (Text.create result.group)
514
+ │ │ ├── react.tsx # React Three Fiber component
515
+ │ │ ├── slug/ # Slug per-fragment curve evaluation
516
+ │ │ ├── core/ # Outline collection and Slug packing
517
+ │ │ └── GlyphOutlineCollector.ts # Collects draw callbacks for vector path
487
518
  │ ├── webgl/ # WebGL mesh buffer utility
488
519
  │ ├── webgpu/ # WebGPU mesh buffer utility
489
520
  │ ├── p5/ # p5.js adapter
@@ -535,11 +566,9 @@ The multi-stage geometry approach (curve polygonization followed by cleanup, the
535
566
 
536
567
  ### Vector rendering
537
568
 
538
- The vector pipeline (`three-text/vector`) renders glyphs directly from their mathematical outlines without tessellation or curve flattening. Text stays sharp at any zoom level and the geometry footprint is small -- just the control points of each curve
539
-
540
- Curves use the [Loop-Blinn](https://www.microsoft.com/en-us/research/wp-content/uploads/2005/01/p1000-loop.pdf) technique: each quadratic curve is rendered as a triangle whose fragment shader evaluates `u² - v` to resolve inside/outside, with screen-space derivatives producing a signed distance that feeds alpha-to-coverage for smooth MSAA edges. Glyph interiors use [Kokojima et al.](https://dl.acm.org/doi/10.1145/1179849.1179997) stencil filling: fan-triangulate, stencil XOR, fill where nonzero
569
+ The vector pipeline (`three-text/vector`) renders glyphs directly from their mathematical outlines without tessellation or curve flattening. Text stays sharp at any zoom level and the geometry footprint is small: one quad per glyph
541
570
 
542
- An alternative for this sort of resolution-independent rendering is [Slug](https://github.com/EricLengyel/Slug) by Eric Lengyel, which casts rays against all curves per fragment to compute winding numbers. Loop-Blinn was chosen here because it integrates with hardware MSAA and alpha-to-coverage directly, without the overhead of adaptive supersampling that Slug requires for comparable antialiasing
571
+ Each quad's fragment shader casts rays against band-indexed curve data to compute a winding number, resolving inside/outside analytically. This is the [Slug](https://jcgt.org/published/0006/02/02/) algorithm by Eric Lengyel. Because winding is evaluated per fragment from the original curves, self-intersecting contours in variable fonts are handled correctly without any special-case geometry processing
543
572
 
544
573
  #### Glyph caching
545
574
 
@@ -765,7 +794,7 @@ const text = await Text.create({
765
794
  // - glyphBaselineY (float): Y coordinate of glyph baseline
766
795
  ```
767
796
 
768
- **Mesh:** attributes live on the extruded `geometry`. **Vector:** the same attributes are emitted on interior, curve, and fill buffer geometries. When you need per-glyph draw ranges (for example stencil passes that must not XOR across overlapping glyphs), use `geometryData.glyphRanges`: each entry lists index/vertex ranges for that glyph’s interior, curve, and fill quads
797
+ **Mesh:** attributes live on the extruded `geometry`. **Vector:** the same attributes are present on the Slug quad mesh, where each glyph is a single quad with `glyphIndex` and `glyphCenter` attributes
769
798
 
770
799
  This option bypasses overlap-based clustering and adds vertex attributes suitable for per-character manipulation in vertex shaders (or TSL `positionNode` displacements). Each unique glyph is still tessellated only once and cached for reuse. The tradeoff is potential visual artifacts where glyphs actually overlap (tight kerning, cursive scripts)
771
800
 
@@ -1240,12 +1269,13 @@ The build generates multiple module formats for core and all adapters:
1240
1269
  **Adapters:**
1241
1270
  - `dist/three/` - Three.js adapter
1242
1271
  - `dist/three/react.js` - React component
1243
- - `dist/vector/` - Vector rendering (Loop-Blinn, Three.js adapter)
1272
+ - `dist/vector/` - Vector rendering (Slug per-fragment curve evaluation)
1244
1273
  - `dist/vector/react.js` - React Three Fiber vector component
1274
+ - `dist/vector/core/` - Framework-agnostic vector core (raw SlugGPUData)
1275
+ - `dist/vector/webgl/` - Raw WebGL2 vector renderer
1276
+ - `dist/vector/webgpu/` - Raw WebGPU vector renderer
1245
1277
  - `dist/webgl/` - WebGL mesh buffer utility
1246
- - `dist/vector/webgl/` - WebGL vector renderer
1247
1278
  - `dist/webgpu/` - WebGPU mesh buffer utility
1248
- - `dist/vector/webgpu/` - WebGPU vector renderer
1249
1279
  - `dist/p5/` - p5.js adapter
1250
1280
 
1251
1281
  **Patterns:**
package/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * @license
3
- * three-text v0.5.1
3
+ * three-text v0.6.0
4
4
  * Copyright © 2025-2026 Jeremy Tribby, Countertype LLC
5
5
  * SPDX-License-Identifier: MIT
6
6
  */
@@ -3263,6 +3263,8 @@ class Text {
3263
3263
  static { this.patternCache = new Map(); }
3264
3264
  static { this.hbInitPromise = null; }
3265
3265
  static { this.fontCache = new Map(); }
3266
+ static { this.fontLoadPromises = new Map(); }
3267
+ static { this.fontRefCounts = new Map(); }
3266
3268
  static { this.fontCacheMemoryBytes = 0; }
3267
3269
  static { this.maxFontCacheMemoryBytes = Infinity; }
3268
3270
  static { this.fontIdCounter = 0; }
@@ -3307,9 +3309,9 @@ class Text {
3307
3309
  if (!Text.hbInitPromise) {
3308
3310
  Text.hbInitPromise = HarfBuzzLoader.getHarfBuzz();
3309
3311
  }
3310
- const loadedFont = await Text.resolveFont(options);
3312
+ const { loadedFont, fontKey } = await Text.resolveFont(options);
3311
3313
  const text = new Text();
3312
- text.setLoadedFont(loadedFont);
3314
+ text.setLoadedFont(loadedFont, fontKey);
3313
3315
  const result = await text.createLayout(options);
3314
3316
  const update = async (newOptions) => {
3315
3317
  const mergedOptions = { ...options };
@@ -3322,8 +3324,8 @@ class Text {
3322
3324
  if (newOptions.font !== undefined ||
3323
3325
  newOptions.fontVariations !== undefined ||
3324
3326
  newOptions.fontFeatures !== undefined) {
3325
- const newLoadedFont = await Text.resolveFont(mergedOptions);
3326
- text.setLoadedFont(newLoadedFont);
3327
+ const { loadedFont: newLoadedFont, fontKey: newFontKey } = await Text.resolveFont(mergedOptions);
3328
+ text.setLoadedFont(newLoadedFont, newFontKey);
3327
3329
  text.resetHelpers();
3328
3330
  }
3329
3331
  options = mergedOptions;
@@ -3344,6 +3346,22 @@ class Text {
3344
3346
  dispose: () => text.destroy()
3345
3347
  };
3346
3348
  }
3349
+ static retainFont(fontKey) {
3350
+ Text.fontRefCounts.set(fontKey, (Text.fontRefCounts.get(fontKey) ?? 0) + 1);
3351
+ }
3352
+ static releaseFont(fontKey, loadedFont) {
3353
+ const nextCount = (Text.fontRefCounts.get(fontKey) ?? 0) - 1;
3354
+ if (nextCount > 0) {
3355
+ Text.fontRefCounts.set(fontKey, nextCount);
3356
+ return;
3357
+ }
3358
+ Text.fontRefCounts.delete(fontKey);
3359
+ // Cached fonts stay alive while present in the cache. If a font has been
3360
+ // evicted, destroy it once the last live handle releases it.
3361
+ if (!Text.fontCache.has(fontKey)) {
3362
+ FontLoader.destroyFont(loadedFont);
3363
+ }
3364
+ }
3347
3365
  static async resolveFont(options) {
3348
3366
  const baseFontKey = typeof options.font === 'string'
3349
3367
  ? options.font
@@ -3357,9 +3375,17 @@ class Text {
3357
3375
  }
3358
3376
  let loadedFont = Text.fontCache.get(fontKey);
3359
3377
  if (!loadedFont) {
3360
- loadedFont = await Text.loadAndCacheFont(fontKey, options.font, options.fontVariations, options.fontFeatures);
3378
+ let loadPromise = Text.fontLoadPromises.get(fontKey);
3379
+ if (!loadPromise) {
3380
+ loadPromise = Text.loadAndCacheFont(fontKey, options.font, options.fontVariations, options.fontFeatures).finally(() => {
3381
+ Text.fontLoadPromises.delete(fontKey);
3382
+ });
3383
+ Text.fontLoadPromises.set(fontKey, loadPromise);
3384
+ }
3385
+ loadedFont = await loadPromise;
3361
3386
  }
3362
- return loadedFont;
3387
+ Text.retainFont(fontKey);
3388
+ return { loadedFont, fontKey };
3363
3389
  }
3364
3390
  static async loadAndCacheFont(fontKey, font, fontVariations, fontFeatures) {
3365
3391
  const tempText = new Text();
@@ -3391,8 +3417,12 @@ class Text {
3391
3417
  const firstKey = Text.fontCache.keys().next().value;
3392
3418
  if (firstKey === undefined)
3393
3419
  break;
3420
+ const font = Text.fontCache.get(firstKey);
3394
3421
  Text.trackFontCacheRemove(firstKey);
3395
3422
  Text.fontCache.delete(firstKey);
3423
+ if ((Text.fontRefCounts.get(firstKey) ?? 0) <= 0 && font) {
3424
+ FontLoader.destroyFont(font);
3425
+ }
3396
3426
  }
3397
3427
  }
3398
3428
  static generateFontContentHash(buffer) {
@@ -3414,8 +3444,12 @@ class Text {
3414
3444
  return `c${++Text.fontIdCounter}`;
3415
3445
  }
3416
3446
  }
3417
- setLoadedFont(loadedFont) {
3447
+ setLoadedFont(loadedFont, fontKey) {
3448
+ if (this.loadedFont && this.loadedFont !== loadedFont) {
3449
+ this.releaseCurrentFont();
3450
+ }
3418
3451
  this.loadedFont = loadedFont;
3452
+ this.currentFontCacheKey = fontKey;
3419
3453
  const contentHash = Text.generateFontContentHash(loadedFont._buffer);
3420
3454
  this.currentFontId = `font_${contentHash}`;
3421
3455
  if (loadedFont.fontVariations) {
@@ -3425,6 +3459,29 @@ class Text {
3425
3459
  this.currentFontId += `_feat_${Text.stableStringify(loadedFont.fontFeatures)}`;
3426
3460
  }
3427
3461
  }
3462
+ releaseCurrentFont() {
3463
+ if (!this.loadedFont)
3464
+ return;
3465
+ const currentFont = this.loadedFont;
3466
+ const currentFontKey = this.currentFontCacheKey;
3467
+ try {
3468
+ if (currentFontKey) {
3469
+ Text.releaseFont(currentFontKey, currentFont);
3470
+ }
3471
+ else {
3472
+ FontLoader.destroyFont(currentFont);
3473
+ }
3474
+ }
3475
+ catch (error) {
3476
+ logger.warn('Error destroying HarfBuzz objects:', error);
3477
+ }
3478
+ finally {
3479
+ this.loadedFont = undefined;
3480
+ this.currentFontCacheKey = undefined;
3481
+ this.textLayout = undefined;
3482
+ this.textShaper = undefined;
3483
+ }
3484
+ }
3428
3485
  async loadFont(fontSrc, fontVariations, fontFeatures) {
3429
3486
  perfLogger.start('Text.loadFont', {
3430
3487
  fontSrc: typeof fontSrc === 'string' ? fontSrc : `buffer(${fontSrc.byteLength})`
@@ -3644,18 +3701,7 @@ class Text {
3644
3701
  if (!this.loadedFont) {
3645
3702
  return;
3646
3703
  }
3647
- const currentFont = this.loadedFont;
3648
- try {
3649
- FontLoader.destroyFont(currentFont);
3650
- }
3651
- catch (error) {
3652
- logger.warn('Error destroying HarfBuzz objects:', error);
3653
- }
3654
- finally {
3655
- this.loadedFont = undefined;
3656
- this.textLayout = undefined;
3657
- this.textShaper = undefined;
3658
- }
3704
+ this.releaseCurrentFont();
3659
3705
  }
3660
3706
  }
3661
3707
 
package/dist/index.d.ts CHANGED
@@ -410,6 +410,8 @@ interface TextOptions {
410
410
  geometryOptimization?: GeometryOptimizationOptions;
411
411
  layout?: LayoutOptions;
412
412
  color?: [number, number, number] | ColorOptions;
413
+ /** Enable rotated RGSS-4 adaptive supersampling (4 samples per pixel). Takes effect when the GLSL rendering path is active. */
414
+ adaptiveSupersampling?: boolean;
413
415
  }
414
416
  interface HyphenationPatternsMap {
415
417
  [language: string]: HyphenationTrieNode;
@@ -457,6 +459,8 @@ declare class Text {
457
459
  private static patternCache;
458
460
  private static hbInitPromise;
459
461
  private static fontCache;
462
+ private static fontLoadPromises;
463
+ private static fontRefCounts;
460
464
  private static fontCacheMemoryBytes;
461
465
  private static maxFontCacheMemoryBytes;
462
466
  private static fontIdCounter;
@@ -465,6 +469,7 @@ declare class Text {
465
469
  private fontLoader;
466
470
  private loadedFont?;
467
471
  private currentFontId;
472
+ private currentFontCacheKey?;
468
473
  private textShaper?;
469
474
  private textLayout?;
470
475
  private constructor();
@@ -472,6 +477,8 @@ declare class Text {
472
477
  static setHarfBuzzBuffer(wasmBuffer: ArrayBuffer): void;
473
478
  static init(): Promise<HarfBuzzInstance>;
474
479
  static create(options: TextOptions): Promise<TextLayoutHandle>;
480
+ private static retainFont;
481
+ private static releaseFont;
475
482
  private static resolveFont;
476
483
  private static loadAndCacheFont;
477
484
  private static trackFontCacheAdd;
@@ -479,6 +486,7 @@ declare class Text {
479
486
  private static enforceFontCacheMemoryLimit;
480
487
  private static generateFontContentHash;
481
488
  private setLoadedFont;
489
+ private releaseCurrentFont;
482
490
  private loadFont;
483
491
  private createLayout;
484
492
  private prepareHyphenation;