three-text 0.2.1 → 0.2.3

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
@@ -17,7 +17,7 @@ A high fidelity 3D font renderer and text layout engine for the web
17
17
 
18
18
  **three-text** renders and formats text from TTF, OTF, and WOFF font files as 3D geometry. It uses [TeX](https://en.wikipedia.org/wiki/TeX)-based parameters for breaking text into paragraphs across multiple lines, and turns font outlines into 3D shapes on the fly, caching their geometries for low CPU overhead in languages with lots of repeating glyphs. Variable fonts are supported as static instances at a given axis coordinate
19
19
 
20
- The library has a framework-agnostic core that returns raw vertex data, with lightweight adapters for Three.js, React Three Fiber, p5.js, WebGL, and WebGPU
20
+ The library has a framework-agnostic core that returns raw vertex data, 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
22
  Under the hood, three-text relies on [HarfBuzz](https://github.com/harfbuzz/harfbuzzjs) for text shaping, [Knuth-Plass](http://www.eprg.org/G53DOC/pdfs/knuth-plass-breaking.pdf) line breaking, [Liang](https://tug.org/docs/liang/liang-thesis.pdf) hyphenation, [libtess2](https://github.com/memononen/libtess2) (based on the [OpenGL Utility Library (GLU) tessellator](https://www.songho.ca/opengl/gl_tessellation.html) by Eric Veach) for removing overlaps and triangulation, bezier 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/).
23
23
 
@@ -25,12 +25,9 @@ Under the hood, three-text relies on [HarfBuzz](https://github.com/harfbuzz/harf
25
25
 
26
26
  - [Overview](#overview)
27
27
  - [Getting started](#getting-started)
28
- - [Three.js](#threejs-usage)
29
- - [React Three Fiber](#react-three-fiber-usage)
30
- - [WebGL](#webgl-usage)
31
- - [WebGPU](#webgpu-usage)
32
- - [p5.js](#p5js-usage)
33
- - [Core (framework-agnostic)](#core-usage)
28
+ - [Three.js](#threejs)
29
+ - [React Three Fiber](#react-three-fiber)
30
+ - [p5.js](#p5js)
34
31
  - [Development and examples](#development-and-examples)
35
32
  - [Architecture](#architecture)
36
33
  - [Why three-text?](#why-three-text)
@@ -70,7 +67,7 @@ three-text has a framework-agnostic core that processes fonts and generates geom
70
67
  - **`three-text/three/react`** - React Three Fiber component
71
68
  - **`three-text/webgl`** - WebGL buffer utility
72
69
  - **`three-text/webgpu`** - WebGPU buffer utility
73
- - **`three-text/p5`** - p5.js geometry converter
70
+ - **`three-text/p5`** - p5.js adapter
74
71
 
75
72
  Choose the import that matches your stack. Most users will use `three-text/three` or `three-text/p5`
76
73
 
@@ -94,34 +91,58 @@ const mesh = new THREE.Mesh(result.geometry, material);
94
91
  scene.add(mesh);
95
92
  ```
96
93
 
94
+ #### React Three Fiber
95
+
96
+ ```jsx
97
+ import { Canvas } from '@react-three/fiber';
98
+ import { Text } from 'three-text/three/react';
99
+
100
+ Text.setHarfBuzzPath('/hb/hb.wasm');
101
+
102
+ function App() {
103
+ return (
104
+ <Canvas>
105
+ <ambientLight />
106
+ <Text font="/fonts/Font.woff" size={72} depth={10}>
107
+ Hello React
108
+ </Text>
109
+ </Canvas>
110
+ );
111
+ }
112
+ ```
113
+
97
114
  #### p5.js
98
115
 
99
116
  ```javascript
100
- import { Text } from 'three-text';
101
- import { createP5Geometry } from 'three-text/p5';
117
+ import 'three-text/p5';
118
+
119
+ let font;
120
+ let textResult;
102
121
 
103
- let textGeom;
122
+ function preload() {
123
+ loadThreeTextShaper('/hb/hb.wasm');
124
+ font = loadThreeTextFont('/fonts/Font.woff');
125
+ }
104
126
 
105
127
  async function setup() {
106
128
  createCanvas(400, 400, WEBGL);
107
- Text.setHarfBuzzPath('/hb/hb.wasm');
108
-
109
- const data = await Text.create({
110
- text: 'Hello p5!',
111
- font: '/fonts/Font.woff',
112
- size: 72
129
+ textResult = await createThreeTextGeometry('Hello p5!', {
130
+ font: font,
131
+ size: 72,
132
+ depth: 30
113
133
  });
114
-
115
- textGeom = createP5Geometry(window, data);
116
134
  }
117
135
 
118
136
  function draw() {
119
137
  background(200);
120
138
  lights();
121
- model(textGeom);
139
+ if (textResult) model(textResult.geometry);
122
140
  }
123
141
  ```
124
142
 
143
+ `createThreeTextGeometry()` accepts all the same options as Three.js (`layout`, `fontVariations`, `depth`, etc.) and returns `{ geometry, planeBounds, glyphs }`. Use `planeBounds` to center the text
144
+
145
+
125
146
  ### Setup
126
147
 
127
148
  The library bundles harfbuzzjs but requires the WASM binary to be available at runtime. You have two options for providing it:
@@ -244,7 +265,7 @@ three-text/
244
265
  │ │ └── ThreeText.tsx # React Three Fiber component
245
266
  │ ├── webgl/ # WebGL buffer utility
246
267
  │ ├── webgpu/ # WebGPU buffer utility
247
- │ ├── p5/ # p5.js geometry converter
268
+ │ ├── p5/ # p5.js adapter
248
269
  │ ├── hyphenation/ # Language-specific hyphenation patterns
249
270
  │ └── utils/ # Performance logging, data structures
250
271
  ├── examples/ # Demos for all adapters
@@ -961,14 +982,14 @@ The build generates multiple module formats for core and all adapters:
961
982
  - `dist/three/react.js` - React component
962
983
  - `dist/webgl/` - WebGL utility
963
984
  - `dist/webgpu/` - WebGPU utility
964
- - `dist/p5/` - p5.js converter
985
+ - `dist/p5/` - p5.js adapter
965
986
 
966
987
  **Patterns:**
967
988
  - `dist/patterns/` - Hyphenation patterns (ESM and UMD)
968
989
 
969
990
  ## Acknowledgements
970
991
 
971
- `three-text` is built on HarfBuzz and TeX, and this library would not exist without the authors and communities who contribute to, support, and steward these projects. Thanks to Theo Honohan and Yasi Perera for advice on graphics
992
+ `three-text` is built on HarfBuzz and TeX, and started as a Three.js project; this library would not exist without the authors and communities who contribute to, support, and steward these projects. Thanks to Theo Honohan and Yasi Perera for the advice on graphics
972
993
 
973
994
  ## License
974
995
 
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * three-text v0.2.1
2
+ * three-text v0.2.3
3
3
  * Copyright (C) 2025 Countertype LLC
4
4
  *
5
5
  * This program is free software: you can redistribute it and/or modify
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * three-text v0.2.1
2
+ * three-text v0.2.3
3
3
  * Copyright (C) 2025 Countertype LLC
4
4
  *
5
5
  * This program is free software: you can redistribute it and/or modify
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * three-text v0.2.1
2
+ * three-text v0.2.3
3
3
  * Copyright (C) 2025 Countertype LLC
4
4
  *
5
5
  * This program is free software: you can redistribute it and/or modify
package/dist/index.min.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * three-text v0.2.1
2
+ * three-text v0.2.3
3
3
  * Copyright (C) 2025 Countertype LLC
4
4
  *
5
5
  * This program is free software: you can redistribute it and/or modify
package/dist/index.umd.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * three-text v0.2.1
2
+ * three-text v0.2.3
3
3
  * Copyright (C) 2025 Countertype LLC
4
4
  *
5
5
  * This program is free software: you can redistribute it and/or modify
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * three-text v0.2.1
2
+ * three-text v0.2.3
3
3
  * Copyright (C) 2025 Countertype LLC
4
4
  *
5
5
  * This program is free software: you can redistribute it and/or modify
package/dist/p5/index.cjs CHANGED
@@ -1,21 +1,30 @@
1
1
  'use strict';
2
2
 
3
- // p5.js adapter - converts core geometry to p5.Geometry
4
- function createP5Geometry(p5Instance, textGeometry) {
5
- // In global mode, p5.Geometry exists but createVector is global
6
- // In instance mode, both are on the instance
7
- const P5Geometry = p5Instance.Geometry || window.p5?.Geometry;
8
- const createVec = p5Instance.createVector || window.createVector;
9
- if (!P5Geometry || !createVec) {
10
- throw new Error('p5.js not found. Make sure p5.js is loaded before calling this function.');
11
- }
12
- const geom = new P5Geometry();
3
+ var Text = require('../index.cjs');
4
+
5
+ // p5.js adapter
6
+ function convertToP5Geometry(p5Instance, textGeometry) {
13
7
  const { vertices, normals, indices } = textGeometry;
14
- // Convert vertices (flip Y for p5.js coordinate system)
8
+ const P5GeometryClass = p5Instance.constructor?.Geometry ||
9
+ (typeof window !== 'undefined' && window.p5?.Geometry);
10
+ if (!P5GeometryClass) {
11
+ throw new Error('p5.Geometry not found. Ensure p5.js is loaded.');
12
+ }
13
+ const geom = new P5GeometryClass();
14
+ const createVec = (x, y, z) => {
15
+ if (typeof p5Instance.createVector === 'function') {
16
+ return p5Instance.createVector(x, y, z);
17
+ }
18
+ const globalCreateVector = (typeof window !== 'undefined' && window.createVector);
19
+ if (globalCreateVector) {
20
+ return globalCreateVector(x, y, z);
21
+ }
22
+ throw new Error('createVector not found');
23
+ };
24
+ // p5 uses +Y up, we use +Y down
15
25
  for (let i = 0; i < vertices.length; i += 3) {
16
26
  geom.vertices.push(createVec(vertices[i], -vertices[i + 1], vertices[i + 2]));
17
27
  }
18
- // Convert normals (flip Y)
19
28
  for (let i = 0; i < normals.length; i += 3) {
20
29
  geom.vertexNormals.push(createVec(normals[i], -normals[i + 1], normals[i + 2]));
21
30
  }
@@ -29,5 +38,75 @@ function createP5Geometry(p5Instance, textGeometry) {
29
38
  }
30
39
  return geom;
31
40
  }
41
+ let shaperInitialized = false;
42
+ if (typeof window !== 'undefined' && window.p5) {
43
+ const p5 = window.p5;
44
+ p5.prototype.loadThreeTextShaper = function (wasmPath) {
45
+ if (shaperInitialized) {
46
+ return;
47
+ }
48
+ Text.Text.setHarfBuzzPath(wasmPath);
49
+ shaperInitialized = true;
50
+ Text.Text.init()
51
+ .then(() => {
52
+ this._decrementPreload();
53
+ })
54
+ .catch((err) => {
55
+ console.error('Failed to load text shaper:', err);
56
+ this._decrementPreload();
57
+ });
58
+ };
59
+ p5.prototype.loadThreeTextFont = function (fontPath, fontVariations) {
60
+ const fontRef = {
61
+ buffer: null,
62
+ path: fontPath,
63
+ variations: fontVariations
64
+ };
65
+ fetch(fontPath)
66
+ .then(res => {
67
+ if (!res.ok) {
68
+ throw new Error(`Failed to load font: HTTP ${res.status}`);
69
+ }
70
+ return res.arrayBuffer();
71
+ })
72
+ .then(buffer => {
73
+ fontRef.buffer = buffer;
74
+ this._decrementPreload();
75
+ })
76
+ .catch((err) => {
77
+ console.error(`Failed to load font ${fontPath}:`, err);
78
+ this._decrementPreload();
79
+ });
80
+ return fontRef;
81
+ };
82
+ p5.prototype.createThreeTextGeometry = async function (text, options) {
83
+ if (!options.font || !options.font.buffer) {
84
+ console.error('Font not loaded. Use loadThreeTextFont() in preload().');
85
+ return null;
86
+ }
87
+ const { font, ...coreOptions } = options;
88
+ try {
89
+ const result = await Text.Text.create({
90
+ text,
91
+ font: font.buffer,
92
+ fontVariations: font.variations,
93
+ ...coreOptions
94
+ });
95
+ const p5Instance = this;
96
+ const geometry = convertToP5Geometry(p5Instance, result);
97
+ return {
98
+ geometry,
99
+ planeBounds: result.planeBounds,
100
+ glyphs: result.glyphs
101
+ };
102
+ }
103
+ catch (err) {
104
+ console.error('Failed to create text geometry:', err);
105
+ return null;
106
+ }
107
+ };
108
+ p5.prototype.registerPreloadMethod('loadThreeTextShaper', p5.prototype);
109
+ p5.prototype.registerPreloadMethod('loadThreeTextFont', p5.prototype);
110
+ }
32
111
 
33
- exports.createP5Geometry = createP5Geometry;
112
+ exports.createP5Geometry = convertToP5Geometry;
@@ -13,7 +13,13 @@ interface P5Geometry {
13
13
  interface P5Instance {
14
14
  Geometry: new () => P5Geometry;
15
15
  createVector(x: number, y: number, z: number): P5Vector;
16
+ _decrementPreload(): void;
16
17
  }
17
- declare function createP5Geometry(p5Instance: P5Instance | any, textGeometry: TextGeometryInfo): P5Geometry;
18
+ declare global {
19
+ interface Window {
20
+ p5?: any;
21
+ }
22
+ }
23
+ declare function convertToP5Geometry(p5Instance: P5Instance, textGeometry: TextGeometryInfo): P5Geometry;
18
24
 
19
- export { createP5Geometry };
25
+ export { convertToP5Geometry as createP5Geometry };
package/dist/p5/index.js CHANGED
@@ -1,19 +1,28 @@
1
- // p5.js adapter - converts core geometry to p5.Geometry
2
- function createP5Geometry(p5Instance, textGeometry) {
3
- // In global mode, p5.Geometry exists but createVector is global
4
- // In instance mode, both are on the instance
5
- const P5Geometry = p5Instance.Geometry || window.p5?.Geometry;
6
- const createVec = p5Instance.createVector || window.createVector;
7
- if (!P5Geometry || !createVec) {
8
- throw new Error('p5.js not found. Make sure p5.js is loaded before calling this function.');
9
- }
10
- const geom = new P5Geometry();
1
+ import { Text } from '../index.js';
2
+
3
+ // p5.js adapter
4
+ function convertToP5Geometry(p5Instance, textGeometry) {
11
5
  const { vertices, normals, indices } = textGeometry;
12
- // Convert vertices (flip Y for p5.js coordinate system)
6
+ const P5GeometryClass = p5Instance.constructor?.Geometry ||
7
+ (typeof window !== 'undefined' && window.p5?.Geometry);
8
+ if (!P5GeometryClass) {
9
+ throw new Error('p5.Geometry not found. Ensure p5.js is loaded.');
10
+ }
11
+ const geom = new P5GeometryClass();
12
+ const createVec = (x, y, z) => {
13
+ if (typeof p5Instance.createVector === 'function') {
14
+ return p5Instance.createVector(x, y, z);
15
+ }
16
+ const globalCreateVector = (typeof window !== 'undefined' && window.createVector);
17
+ if (globalCreateVector) {
18
+ return globalCreateVector(x, y, z);
19
+ }
20
+ throw new Error('createVector not found');
21
+ };
22
+ // p5 uses +Y up, we use +Y down
13
23
  for (let i = 0; i < vertices.length; i += 3) {
14
24
  geom.vertices.push(createVec(vertices[i], -vertices[i + 1], vertices[i + 2]));
15
25
  }
16
- // Convert normals (flip Y)
17
26
  for (let i = 0; i < normals.length; i += 3) {
18
27
  geom.vertexNormals.push(createVec(normals[i], -normals[i + 1], normals[i + 2]));
19
28
  }
@@ -27,5 +36,75 @@ function createP5Geometry(p5Instance, textGeometry) {
27
36
  }
28
37
  return geom;
29
38
  }
39
+ let shaperInitialized = false;
40
+ if (typeof window !== 'undefined' && window.p5) {
41
+ const p5 = window.p5;
42
+ p5.prototype.loadThreeTextShaper = function (wasmPath) {
43
+ if (shaperInitialized) {
44
+ return;
45
+ }
46
+ Text.setHarfBuzzPath(wasmPath);
47
+ shaperInitialized = true;
48
+ Text.init()
49
+ .then(() => {
50
+ this._decrementPreload();
51
+ })
52
+ .catch((err) => {
53
+ console.error('Failed to load text shaper:', err);
54
+ this._decrementPreload();
55
+ });
56
+ };
57
+ p5.prototype.loadThreeTextFont = function (fontPath, fontVariations) {
58
+ const fontRef = {
59
+ buffer: null,
60
+ path: fontPath,
61
+ variations: fontVariations
62
+ };
63
+ fetch(fontPath)
64
+ .then(res => {
65
+ if (!res.ok) {
66
+ throw new Error(`Failed to load font: HTTP ${res.status}`);
67
+ }
68
+ return res.arrayBuffer();
69
+ })
70
+ .then(buffer => {
71
+ fontRef.buffer = buffer;
72
+ this._decrementPreload();
73
+ })
74
+ .catch((err) => {
75
+ console.error(`Failed to load font ${fontPath}:`, err);
76
+ this._decrementPreload();
77
+ });
78
+ return fontRef;
79
+ };
80
+ p5.prototype.createThreeTextGeometry = async function (text, options) {
81
+ if (!options.font || !options.font.buffer) {
82
+ console.error('Font not loaded. Use loadThreeTextFont() in preload().');
83
+ return null;
84
+ }
85
+ const { font, ...coreOptions } = options;
86
+ try {
87
+ const result = await Text.create({
88
+ text,
89
+ font: font.buffer,
90
+ fontVariations: font.variations,
91
+ ...coreOptions
92
+ });
93
+ const p5Instance = this;
94
+ const geometry = convertToP5Geometry(p5Instance, result);
95
+ return {
96
+ geometry,
97
+ planeBounds: result.planeBounds,
98
+ glyphs: result.glyphs
99
+ };
100
+ }
101
+ catch (err) {
102
+ console.error('Failed to create text geometry:', err);
103
+ return null;
104
+ }
105
+ };
106
+ p5.prototype.registerPreloadMethod('loadThreeTextShaper', p5.prototype);
107
+ p5.prototype.registerPreloadMethod('loadThreeTextFont', p5.prototype);
108
+ }
30
109
 
31
- export { createP5Geometry };
110
+ export { convertToP5Geometry as createP5Geometry };
@@ -19,8 +19,8 @@ class Text {
19
19
  const geometry = new three.BufferGeometry();
20
20
  geometry.setAttribute('position', new three.Float32BufferAttribute(coreResult.vertices, 3));
21
21
  geometry.setAttribute('normal', new three.Float32BufferAttribute(coreResult.normals, 3));
22
- geometry.setIndex(Array.from(coreResult.indices));
23
- // Add optional attributes
22
+ geometry.setIndex(new three.Uint32BufferAttribute(coreResult.indices, 1));
23
+ // Add optional color attribute (only if provided)
24
24
  if (coreResult.colors) {
25
25
  geometry.setAttribute('color', new three.Float32BufferAttribute(coreResult.colors, 3));
26
26
  }
@@ -1,4 +1,4 @@
1
- import { BufferGeometry, Float32BufferAttribute } from 'three';
1
+ import { BufferGeometry, Float32BufferAttribute, Uint32BufferAttribute } from 'three';
2
2
  import { Text as Text$1 } from '../index.js';
3
3
 
4
4
  // Three.js adapter - wraps core text processing and returns BufferGeometry
@@ -17,8 +17,8 @@ class Text {
17
17
  const geometry = new BufferGeometry();
18
18
  geometry.setAttribute('position', new Float32BufferAttribute(coreResult.vertices, 3));
19
19
  geometry.setAttribute('normal', new Float32BufferAttribute(coreResult.normals, 3));
20
- geometry.setIndex(Array.from(coreResult.indices));
21
- // Add optional attributes
20
+ geometry.setIndex(new Uint32BufferAttribute(coreResult.indices, 1));
21
+ // Add optional color attribute (only if provided)
22
22
  if (coreResult.colors) {
23
23
  geometry.setAttribute('color', new Float32BufferAttribute(coreResult.colors, 3));
24
24
  }
@@ -72,13 +72,6 @@ const Text$1 = react.forwardRef(function Text(props, ref) {
72
72
  font,
73
73
  ...memoizedTextOptions,
74
74
  });
75
- if (text.geometry &&
76
- !text.geometry.attributes.color &&
77
- vertexColors) {
78
- const vertexCount = text.geometry.attributes.position.count;
79
- const colors = new Float32Array(vertexCount * 3).fill(1.0);
80
- text.geometry.setAttribute("color", new THREE__namespace.BufferAttribute(colors, 3));
81
- }
82
75
  if (!cancelled) {
83
76
  setGeometry(text.geometry);
84
77
  if (onLoad)
@@ -51,13 +51,6 @@ const Text$1 = forwardRef(function Text(props, ref) {
51
51
  font,
52
52
  ...memoizedTextOptions,
53
53
  });
54
- if (text.geometry &&
55
- !text.geometry.attributes.color &&
56
- vertexColors) {
57
- const vertexCount = text.geometry.attributes.position.count;
58
- const colors = new Float32Array(vertexCount * 3).fill(1.0);
59
- text.geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
60
- }
61
54
  if (!cancelled) {
62
55
  setGeometry(text.geometry);
63
56
  if (onLoad)
@@ -12,6 +12,12 @@ interface P5Geometry {
12
12
  interface P5Instance {
13
13
  Geometry: new () => P5Geometry;
14
14
  createVector(x: number, y: number, z: number): P5Vector;
15
+ _decrementPreload(): void;
15
16
  }
16
- export declare function createP5Geometry(p5Instance: P5Instance | any, textGeometry: TextGeometryInfo): P5Geometry;
17
- export {};
17
+ declare global {
18
+ interface Window {
19
+ p5?: any;
20
+ }
21
+ }
22
+ declare function convertToP5Geometry(p5Instance: P5Instance, textGeometry: TextGeometryInfo): P5Geometry;
23
+ export { convertToP5Geometry as createP5Geometry };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "three-text",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "3D font rendering and text layout engine for the web",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",