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.
- package/LICENSE_THIRD_PARTY +15 -0
- package/README.md +80 -50
- package/dist/index.cjs +66 -20
- package/dist/index.d.ts +8 -0
- package/dist/index.js +66 -20
- package/dist/index.min.cjs +310 -307
- package/dist/index.min.js +265 -262
- package/dist/index.umd.js +68 -21
- package/dist/index.umd.min.js +268 -265
- package/dist/three/index.cjs +2 -1
- package/dist/three/index.d.ts +1 -0
- package/dist/three/index.js +2 -1
- package/dist/three/react.cjs +35 -17
- package/dist/three/react.d.ts +8 -0
- package/dist/three/react.js +35 -17
- package/dist/types/core/Text.d.ts +6 -0
- package/dist/types/core/types.d.ts +2 -33
- package/dist/types/three/index.d.ts +1 -0
- package/dist/types/vector/core/index.d.ts +28 -0
- package/dist/types/vector/index.d.ts +17 -12
- package/dist/types/vector/react.d.ts +3 -4
- package/dist/types/vector/slug/SlugPacker.d.ts +2 -0
- package/dist/types/vector/slug/curveUtils.d.ts +6 -0
- package/dist/types/vector/slug/index.d.ts +8 -0
- package/dist/types/vector/slug/shaderStrings.d.ts +4 -0
- package/dist/types/vector/slug/slugGLSL.d.ts +21 -0
- package/dist/types/vector/slug/slugTSL.d.ts +13 -0
- package/dist/types/vector/slug/types.d.ts +30 -0
- package/dist/types/vector/slug/unpackVertices.d.ts +11 -0
- package/dist/types/vector/webgl/index.d.ts +7 -3
- package/dist/types/vector/webgpu/index.d.ts +4 -4
- package/dist/vector/all.cjs +21 -0
- package/dist/vector/all.d.ts +134 -0
- package/dist/vector/all.js +2 -0
- package/dist/vector/core/index.cjs +856 -0
- package/dist/vector/core/index.d.ts +63 -0
- package/dist/vector/core/index.js +854 -0
- package/dist/vector/core.cjs +5489 -0
- package/dist/vector/core.d.ts +402 -0
- package/dist/vector/core.js +5486 -0
- package/dist/vector/index.cjs +5 -1305
- package/dist/vector/index.d.ts +41 -67
- package/dist/vector/index.js +3 -1306
- package/dist/vector/index2.cjs +287 -0
- package/dist/vector/index2.js +264 -0
- package/dist/vector/loopBlinnTSL.d.ts +69 -0
- package/dist/vector/react.cjs +54 -40
- package/dist/vector/react.d.ts +11 -2
- package/dist/vector/react.js +55 -41
- package/dist/vector/slugTSL.cjs +252 -0
- package/dist/vector/slugTSL.js +231 -0
- package/dist/vector/webgl/index.cjs +131 -201
- package/dist/vector/webgl/index.d.ts +19 -44
- package/dist/vector/webgl/index.js +131 -201
- package/dist/vector/webgpu/index.cjs +100 -283
- package/dist/vector/webgpu/index.d.ts +16 -45
- package/dist/vector/webgpu/index.js +100 -283
- package/package.json +6 -1
- package/dist/types/vector/GlyphVectorGeometryBuilder.d.ts +0 -26
- package/dist/types/vector/LoopBlinnGeometry.d.ts +0 -68
- package/dist/types/vector/loopBlinnTSL.d.ts +0 -11
package/LICENSE_THIRD_PARTY
CHANGED
|
@@ -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** (
|
|
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 [
|
|
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 (
|
|
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/
|
|
77
|
-
- **`three-text/vector/
|
|
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
|
|
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`
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
441
|
-
- **Vector
|
|
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
|
|
486
|
+
### Why Slug
|
|
454
487
|
|
|
455
|
-
The vector path uses
|
|
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 (
|
|
480
|
-
│ │ ├── index.ts #
|
|
481
|
-
│ │ ├──
|
|
482
|
-
│ │ ├──
|
|
483
|
-
│ │ ├──
|
|
484
|
-
│ │
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|