three-text
A high fidelity font renderer and text layout engine for Three.js
Overview
Caution
three-text is an alpha release and the API may break rapidly. This warning will last at least through the end of 2025. 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
three-text renders and formats text from TTF, OTF, and WOFF font files in Three.js. It uses 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 C…
three-text
A high fidelity font renderer and text layout engine for Three.js
Overview
Caution
three-text is an alpha release and the API may break rapidly. This warning will last at least through the end of 2025. 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
three-text renders and formats text from TTF, OTF, and WOFF font files in Three.js. It uses 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
Under the hood, three-text relies on HarfBuzz for text shaping, Knuth-Plass line breaking, Liang hyphenation, libtess2 (based on the OpenGL Utility Library (GLU) tessellator by Eric Veach) for removing overlaps and triangulation, bezier curve polygonization from Maxim Shemanarev’s Anti-Grain Geometry, Visvalingam-Whyatt line simplification, and Three.js as a platform for 3D rendering on the web
Table of contents
- Overview
- Getting started
- Development and examples
- Why three-text?
- Library structure
- Key concepts and methods
- Configuration
- Querying text content
- API reference
- Memory management
- Debugging
- Browser compatibility
- Testing
- Build system
- Build outputs
- Acknowledgements
- License
Getting started
To use three-text in your own project:
npm install three-text three
harfbuzzjs is a direct dependency and will be installed automatically
Setup
The library bundles harfbuzzjs but requires the WASM binary to be available at runtime. You have two options for providing it:
Option 1: Path-Based Loading (recommended for most uses)
This is the simplest and recommended approach. The library’s internal caching ensures the WASM file is fetched only once, even if you create multiple Text instances
Copy the WASM binary to a public directory:
cp node_modules/harfbuzzjs/hb.wasm public/hb/
Then, before any Text.create() calls, tell three-text where to find it:
import { Text } from 'three-text';
Text.setHarfBuzzPath('/hb/hb.wasm');
Option 2: Buffer-based loading
This method is essential for applications that use Web Workers, as it is the only way to share a single fetched resource across multiple threads. It gives you full control over loading and prevents each worker from re-downloading the WASM binary
import { Text } from 'three-text';
// On your main thread, fetch the assets once.
const wasmResponse = await fetch('/hb/hb.wasm');
const wasmBuffer = await wasmResponse.arrayBuffer();
// Then, in each of your Web Workers, receive the buffer and
// provide it to the library before use.
// worker.js:
self.onmessage = (e) => {
const { wasmBuffer } = e.data;
Text.setHarfBuzzBuffer(wasmBuffer);
// ... now you can call Text.create()
};
The library will prioritize the buffer if both a path and a buffer have been set
Hyphenation patterns
For ES Modules (recommended): Import and register only the languages you need:
import enUs from 'three-text/patterns/en-us';
import { Text } from 'three-text';
Text.registerPattern('en-us', enUs);
For UMD builds: Copy patterns to your public directory and load via script tags:
cp -r node_modules/three-text/dist/patterns public/patterns/
Basic usage
For modern browsers that support ES Modules:
<script type="module">
import { Text } from 'three-text';
import fr from 'three-text/patterns/fr';
// Configure once
Text.setHarfBuzzPath('/hb/hb.wasm');
Text.registerPattern('fr', fr);
const text = await Text.create({
text: 'Bonjour',
font: '/fonts/YourFont.woff', // or .ttf, .otf
size: 72,
layout: { width: 400, align: 'center', language: 'fr' },
});
</script>
UMD (legacy browser) usage
<!-- Load Three.js and three-text -->
<script src="three-text/dist/index.umd.js"></script>
<!-- Load hyphenation patterns as needed (auto-registers) -->
<script src="/patterns/fr.umd.js"></script>
<script>
const { Text } = window.ThreeText;
// Configure once
Text.setHarfBuzzPath('/hb/hb.wasm');
const text = await Text.create({
text: 'Bonjour',
font: '/fonts/YourFont.woff', // or .ttf, .otf
size: 72,
layout: { width: 400, align: 'center', language: 'fr' }
});
</script>
Patterns loaded via script tags automatically register themselves this way
React Three Fiber usage
For react-three-fiber projects, use the <ThreeText> component which manages font loading, geometry creation, and React’s lifecycle:
import { ThreeText } from 'three-text/react';
function Scene() {
return (
<ThreeText
font="/fonts/Font-Regular.woff"
size={72}
depth={100}
layout={{
hyphenate: true,
language: 'fr',
}}
onLoad={(geometry, info) => console.log('Text loaded!', info)}
>
Hello world
</ThreeText>
);
}
All TextOptions flow through as props, alongside standard Three.js mesh properties:
<ThreeText
font="/fonts/MyFont.woff"
size={100}
depth={100}
lineHeight={1.2}
letterSpacing={0.02}
fontVariations={{ wght: 500, wdth: 100 }}
layout={{
width: 800,
align: 'justify',
language: 'en-us',
}}
position={[0, 0, 0]}
rotation={[0, Math.PI / 4, 0]}
material={customMaterial}
vertexColors={true} // Default: true
onLoad={(geometry, info) => {}}
onError={(error) => {}}
>
Your text content here
</ThreeText>
Vertex colors are included by default for maximum compatibility with custom materials. Set vertexColors={false} to disable them
Development and examples
three-text is built with TypeScript, and requires Node for compilation. If you don’t already have Node installed on your system, visit nodejs.org to download and install it
To clone the repo and try the demo:
git clone --recurse-submodules git@github.com:countertype/three-text.git
cd three-text
npm install
npm run build
npm run serve
Then navigate to http://127.0.0.1:8080/examples/
Although Three.js has deprecated UMD, for maximum device support there is also an example of the library without ESM at http://127.0.0.1:8080/examples/index-umd.html
For React developers, there’s also a React Three Fiber example with Vite and Leva GUI controls:
cd examples/react-three-fiber
npm install
npm run dev
Then navigate to http://localhost:3000
Why three-text?
three-text was designed to produce high-fidelity, 3D mesh geometry for Three.js scenes
Existing Three.js text solutions take different approaches:
- Three.js native TextGeometry uses fonts converted by facetype.js to JSON format. It creates 3D text by extruding flat 2D character outlines. While this produces true 3D geometry with depth, there is no support for real fonts or OpenType features needed for many of the world’s scripts
- three-bmfont-text is a 2D approach, using pre-rendered bitmap fonts with SDF support. Texture atlases are generated at specific sizes, and artifacts are apparent up close
- troika-three-text uses MSDF, which improves quality, and like three-text, it is built on HarfBuzz, which provides substantial language coverage, but is ultimately a 2D technique in image space. For flat text that does not need formatting or extrusion, and where artifacts are acceptable up close, troika works well
three-text generates true 3D geometry from font files via HarfBuzz. It is sharper at close distances than three-bmfont-text and troika-three-text when flat, and can fully participate in the scene as a THREE.BufferGeometry. The library caches tesselated glyphs, so a paragraph of 1000 words might only require 50 tessellations depending on the language and only one draw call is made. This makes it well-suited to longer texts. In addition to performance considerations, three-text provides control over typesetting and paragraph justification via TeX-based parameters
Library structure
three-text/
├── src/
│ ├── core/ # Core text rendering engine
│ │ ├── Text.ts # Main Text class, user-facing API
│ │ ├── types.ts # Shared TypeScript interfaces and types
│ │ ├── cache/ # Glyph caching system
│ │ ├── font/ # Font loading and metrics
│ │ ├── shaping/ # HarfBuzz text shaping
│ │ ├── layout/ # Line breaking and text layout
│ │ └── geometry/ # Tessellation and geometry processing
│ ├── react/ # React Three Fiber components
│ ├── hyphenation/ # Language-specific hyphenation patterns
│ └── utils/ # Performance logging, data structures
├── scripts/ # Scripts for converting hyphenation patterns and more
├── examples/ # Demos and usage examples
└── dist/ # Built library (ESM, CJS, UMD) including patterns
Key concepts and methods
Text shaping
Text shaping is the process of converting a string of Unicode text into positioned glyphs. This is handled entirely by HarfBuzz, which processes OTF and TTF font binaries, shaping them according to the OpenType specification by applying features like kerning, contextual alternates, mark positioning, and diacritic placement. A font and a text string go in; low-level drawing instructions come out
Line breaking
For text justification, the Knuth-Plass algorithm finds optimal line breaks by minimizing the total “badness” of a paragraph. Unlike greedy algorithms that make locally optimal (per-line) decisions, Knuth-Plass considers all possible break points across the entire paragraph
The algorithm models text using three fundamental elements:
- Boxes: Non-breakable content such as letters, words, or inline objects
- Glue: Stretchable and shrinkable spaces between boxes with natural width, maximum stretch, and maximum shrink values
- Penalties: Potential break points with associated costs, including hyphenation points and explicit breaks
Line badness is calculated based on how much glue must stretch or shrink from its natural width to achieve the target line length. The algorithm finds the sequence of breaks that minimizes total badness across the paragraph
This uses a three-pass approach: first without hyphenation (pretolerance), then with hyphenation (tolerance), and finally with emergency stretch for difficult paragraphs that cannot be broken acceptably
Hyphenation
Hyphenation uses patterns derived from the Tex hyphenation project, converted into optimized trie structures for efficient lookup. The library supports over 70 languages with patterns that follow Liang’s algorithm for finding valid hyphenation points while avoiding false positives
Geometry generation and optimization
To optimize performance, three-text generates the geometry for each unique glyph or glyph cluster only once. The result is stored in a cache for reuse. This initial geometry creation is a multi-stage pipeline:
- Path collection: HarfBuzz callbacks provide low level drawing operations
- Curve polygonization: Uses Anti-Grain Geometry’s recursive subdivision to convert bezier curves into polygons, concentrating points where curvature is high
- Geometry optimization:
- Visvalingam-Whyatt simplification: removes vertices that contribute the least to the overall shape, preserving sharp corners and subtle curves
- Colinear point removal: eliminates redundant points that lie on straight lines within angle tolerances
- Overlap removal: removes self-intersections and resolves overlapping paths between glyphs, preserving correct winding rules for triangulation.
- Triangulation: converts cleaned 2D shapes into triangles using libtess2 with non-zero winding rule
- Mesh construction: generates 2D or 3D geometry with front faces and optional depth/extrusion (back faces and side walls)
The multi-stage geometry approach (curve polygonization followed by cleanup, then triangulation) can reduce triangle counts while maintaining high visual fidelity and removing overlaps in variable fonts
Glyph caching
The library uses a hybrid caching strategy to maximize performance while ensuring visual correctness
By default, it operates with glyph-level cache. The geometry for each unique character (a, b, c...) is generated only once and stored for reuse to avoiding redundant computation
For text with tight tracking, connected scripts, or complex kerning pairs, individual glyphs can overlap. When an overlap within a word is found, the entire word is treated as a single unit and escalated to a word-level cache. All of its glyphs are tessellated together to correctly resolve the overlaps, and the resulting geometry for the word is cached
Flat geometry mode
When depth is 0, the library generates single-sided geometry and relies on THREE.DoubleSide materials for back face rendering, reducing triangles by approximately 50%. Custom shaders may need to handle normal flipping for consistent lighting on both sides
Configuration
Curve fidelity
The library converts bezier curves into line segments by recursively subdividing curves until they meet specified quality thresholds. This is based on the AGG library, attempting to place vertices only where they are needed to maintain the integrity of the curve. You can control curve fidelity with distanceTolerance and angleTolerance
distanceTolerance: The maximum allowed deviation of the curve from a straight line segment, measured in font units. Lower values produce higher fidelity and more vertices. Default is0.5, which is nearly imperceptable without extrusionangleTolerance: The maximum angle in radians between segments at a join. This helps preserve sharp corners. Default is0.2
In general, this step helps more with time to first render than ongoing interactions in the scene.
// Using the default configuration
const text = await Text.create({
text: 'Sample text',
font: '/fonts/Font.ttf',
size: 72,
});
const text = await Text.create({
text: 'Sample text',
font: '/fonts/Font.ttf',
curveFidelity: {
distanceTolerance: 0.2, // Tighter tolerance for smoother curves
angleTolerance: 0.1, // Sharper angle preservation
},
});
Geometry optimization
three-text uses a line simplification algorithm after creating lines to reduce the complexity of the shapes as well, which can be combined with curveFidelity for different types of control. It is enabled by default:
// Default optimization (automatic)
const text = await Text.create({
text: 'Sample text',
font: '/fonts/Font.ttf',
});
// Custom optimization settings
```javascript
const text = await Text.create({
text: 'Sample text',
font: '/fonts/Font.ttf',
geometryOptimization: {
areaThreshold: 1.0, // Default: 1.0 (remove triangles < 1 font unit²)
colinearThreshold: 0.0087, // Default: ~0.5° in radians
minSegmentLength: 10, // Default: 10 font units
},
});
The Visvalingam-Whyatt simplification removes vertices whose removal creates triangles with area below the threshold
Colinear point removal eliminates redundant vertices that lie on straight lines within the specified angle tolerance
The default settings provide a significant reduction while maintaining high visual quality, but won’t be perfect for every font. Adjust thresholds based on your quality requirements, performance constraints, and testing
Line breaking parameters
The Knuth-Plass algorithm provides extensive control over line breaking quality:
Basic parameters
- pretolerance (100): Maximum badness for the first pass without hyphenation
- tolerance (800): Maximum badness for the second pass with hyphenation
- emergencyStretch (0): Additional stretchability for difficult paragraphs
- autoEmergencyStretch (0.1): Emergency stretch as percentage of line width (e.g., 0.1 = 10%). Defaults to 10% for non-hyphenated text
- disableSingleWordDetection (false): Disable automatic prevention of short single-word lines
Advanced parameters
- linepenalty (10): Base penalty added to each line’s badness before squaring
- looseness (0): Try to make the paragraph this many lines longer (positive) or shorter (negative)
Hyphenation control
- lefthyphenmin (2): Minimum characters before a hyphen
- righthyphenmin (4): Minimum characters after a hyphen
- hyphenpenalty (50): Penalty for breaking at automatic hyphenation points
- exhyphenpenalty (50): Penalty for breaking at explicit hyphens
- doublehyphendemerits (10000): Additional demerits for consecutive hyphenated lines
Line quality
- adjdemerits (10000): Demerits when adjacent lines have incompatible fitness classes (very tight next to very loose)
Lower penalty/tolerance values produce tighter spacing but may fail to find acceptable breaks for challenging text
Single-word line detection
By default, the library detects and prevents short single-word lines (words occupying less than 50% of the line width on non-final lines) by iteratively applying emergency stretch. This can be disabled if needed:
const text = await Text.create({
text: 'Your text content',
font: '/fonts/Font.ttf',
layout: {
width: 1000,
disableSingleWordDetection: true,
},
});
Hyphenation
Import and register patterns statically for better tree-shaking:
import enUs from 'three-text/patterns/en-us';
import { Text } from 'three-text';
Text.setHarfBuzzPath('/hb/hb.wasm');
Text.registerPattern('en-us', enUs);
const text = await Text.create({
text: 'Long text content',
font: '/fonts/Font.ttf',
layout: {
width: 400,
language: 'en-us',
},
});
Alternative: Patterns can also load dynamically where preferred (requires pattern files to be deployed):
const text = await Text.create({
text: 'Long text content',
font: '/fonts/Font.ttf',
layout: {
width: 400,
language: 'fr',
patternsPath: '/patterns/', // Optional, defaults to '/patterns/'
},
});
Per-glyph animation attributes
For shader-based animations and interactive effects, the library can generate per-vertex attributes that identify which glyph each vertex belongs to:
const text = await Text.create({
text: 'Sample text',
font: '/fonts/Font.ttf',
separateGlyphsWithAttributes: true,
});
// Geometry includes these vertex attributes:
// - glyphCenter (vec3): center point of each glyph
// - glyphIndex (float): sequential glyph index
// - glyphLineIndex (float): line number
This option bypasses overlap-based clustering and adds vertex attributes suitable for per-character manipulation in vertex shaders. 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)
Variable fonts
Variable fonts allow dynamic adjustment of typographic characteristics through variation axes:
const text = await Text.create({
text: 'Sample text',
font: '/fonts/VariableFont.ttf',
fontVariations: {
wght: 700, // Weight
wdth: 125, // Width
slnt: -15, // Slant
opsz: 14, // Optical size
},
});
As long as the axis is valid, it will be available by its 4-character tag
Axis information and STAT table support
The library automatically extracts axis information from variable fonts, including human-readable names from the Style Attributes (STAT) table when available:
const loadedFont = text.getLoadedFont();
if (loadedFont?.variationAxes) {
console.log(loadedFont.variationAxes);
// Output for fonts with STAT table:
// {
// wght: { min: 100, default: 400, max: 900, name: "Weight" },
// wdth: { min: 75, default: 100, max: 125, name: "Width" },
// opsz: { min: 8, default: 14, max: 144, name: "Optical Size" },
// XOPQ: { min: 27, default: 96, max: 175, name: "Parametric Thick Stroke" }
// }
}
For fonts with a STAT table, human-readable axis names are automatically extracted. This enables user interfaces to display “Weight” instead of “wght”, or “Optical Size” instead of “opsz”. Custom parametric axes will also have their proper names extracted if defined
Axis values are applied through HarfBuzz, which handles the interpolation between master designs
The library automatically removes overlaps (self-intersections) in variable fonts. Static fonts skip this step by default, but a removeOverlaps parameter can be set to false
const text = await Text.create({
text: 'Sample text',
font: '/fonts/VariableFont.ttf',
fontVariations: { wght: 500 },
removeOverlaps: false,
});
Querying text content
After creating text geometry, use the query() method to find text ranges:
const text = await Text.create({
text: 'Contact us at hello@example.com or visit our website',
font: '/fonts/Font.ttf',
layout: { width: 800, align: 'justify' },
});
const ranges = text.query({
byText: ['Contact', 'website'],
});
Each range contains:
- start/end character indices
- bounds: array of bounding boxes (multiple if text spans lines)
- glyphs: relevant glyph geometry data
- lineIndices: which lines the range spans
Query types
The API supports two query strategies:
Text matching
// Find exact text matches (case-sensitive)
const ranges = text.query({
byText: ['hello', 'world', 'Hello world'],
});
Character ranges
// Direct character index ranges
const ranges = text.query({
byCharRange: [
{ start: 0, end: 5 }, // First 5 characters
{ start: 10, end: 20 }, // Characters 10-20
],
});
Combining query types
Multiple query types can be used together:
const ranges = text.query({
byText: ['OpenType', 'TypeScript'],
byCharRange: [{ start: 0, end: 5 }],
});
// Returns all matches as a single TextRange[] array
Text coloring
The color option accepts either a single RGB array for uniform coloring or an object for selective coloring. Coloring is applied during geometry creation, after line breaking and hyphenation
// Uniform coloring
const text = await Text.create({
text: 'Hello world',
font: '/fonts/Font.ttf',
color: [1, 0.5, 0],
});
// Selective coloring
const text = await Text.create({
text: 'Warning: connection failed at line 42',
font: '/fonts/Font.ttf',
color: {
default: [1, 1, 1],
byText: {
Warning: [1, 0, 0],
connection: [1, 1, 0],
},
// byCharRange: [{ start: 35, end: 37, color: [0, 1, 1] }],
},
});
Text matching occurs after layout processing, so patterns like “connection” will be found even if hyphenation splits them across lines. The coloredRanges property on the returned object contains the resolved color assignments for programmatic access to the colored parts of the geometry
API reference
The library’s full TypeScript definitions are the most complete source of truth for the API. The core data structures and configuration options can be found in src/core/types.ts
Text class
Static Methods
Text.create(options: TextOptions): Promise<TextGeometryInfo>
Creates text geometry with automatic font loading and HarfBuzz initialization. This is the primary API for all text rendering. Returns a result object with:
geometry: BufferGeometry- Three.js geometry ready for renderingglyphs: GlyphGeometryInfo[]- Per-glyph informationplaneBounds- Overall text boundsstats- Performance and optimization statisticsquery(options)- Method to find text rangesgetLoadedFont()- Access to font metadata and variable font axesgetCacheStatistics()- Glyph cache performance dataclearCache()- Clear the glyph geometry cache for this instancemeasureTextWidth(text, letterSpacing?)- Measure text width in font units
Text.setHarfBuzzPath(path: string): void
Required. Sets the path for the HarfBuzz WASM binary. Must be called before Text.create()
Text.registerPattern(language: string, pattern: HyphenationTrieNode): void
Registers a hyphenation pattern for a language. Use with static imports for tree-shaking
Text.init(): Promise<HarfBuzzInstance>
Initializes HarfBuzz WebAssembly. Called automatically by create(), but can be called explicitly for early initialization
Text.preloadPatterns(languages: string[]): Promise<void>
Preloads hyphenation patterns for specified languages. Useful for avoiding async pattern loading during text rendering
Instance Methods
The following methods are available on instances created by Text.create():
getFontMetrics(): FontMetrics
Returns font metrics including ascender, descender, line gap, and units per em. Useful for text layout calculations
Key Interfaces
Below are the most important configuration interfaces. For a complete list of all properties and data structures, see src/core/types.ts
TextOptions
interface TextOptions {
text: string; // Text content to render
font?: string | ArrayBuffer; // Font file path or buffer (TTF, OTF, or WOFF)
size?: number; // Font size in scene units (default: 72)
depth?: number; // Extrusion depth (default: 0)
lineHeight?: number; // Line height multiplier (default: 1.0)
letterSpacing?: number; // Letter spacing as a fraction of em (e.g., 0.05)
fontVariations?: { [key: string]: number }; // Variable font axis settings
removeOverlaps?: boolean; // Override default overlap removal (auto-enabled for VF only)
separateGlyphsWithAttributes?: boolean; // Force individual glyph tessellation and add shader attributes
color?: [number, number, number] | ColorOptions; // Text coloring (simple or complex)
// Configuration for geometry generation and layout
curveFidelity?: CurveFidelityConfig;
geometryOptimization?: GeometryOptimizationOptions;
layout?: LayoutOptions;
}
interface ColorOptions {
default?: [number, number, number]; // Default color for all text
byText?: { [text: string]: [number, number, number] }; // Color specific text matches
byCharRange?: {
start: number;
end: number;
color: [number, number, number];
}[]; // Color character ranges
}
LayoutOptions
interface LayoutOptions {
width?: number; // Line width in scene units
align?: 'left' | 'center' | 'right' | 'justify';
direction?: 'ltr' | 'rtl';
respectExistingBreaks?: boolean; // Preserve line breaks in input text (default: true)
hyphenate?: boolean; // Enable hyphenation
language?: string; // Language code for hyphenation (e.g., 'en-us')
patternsPath?: string; // Optional base path for dynamic pattern loading (default: '/patterns/')
hyphenationPatterns?: HyphenationPatternsMap; // Pre-loaded pattern data
// Knuth-Plass line breaking parameters:
tolerance?: number; // Maximum badness for second pass (default: 800)
pretolerance?: number; // Maximum badness for first pass (default: 100)
emergencyStretch?: number; // Additional stretchability for difficult paragraphs
autoEmergencyStretch?: number; // Emergency stretch as percentage of line width (defaults to 10% for non-hyphenated)
disableSingleWordDetection?: boolean; // Disable automatic single-word line prevention (default: false)
lefthyphenmin?: number; // Minimum character
// s before hyphen (default: 2)
righthyphenmin?: number; // Minimum characters after hyphen (default: 4)
linepenalty?: number; // Base penalty per line (default: 10)
adjdemerits?: number; // Penalty for incompatible fitness classes (default: 10000)
hyphenpenalty?: number; // Penalty for automatic hyphenation (default: 50)
exhyphenpenalty?: number; // Penalty for explicit hyphens (default: 50)
doublehyphendemerits?: number; // Penalty for consecutive hyphenated lines (default: 10000)
looseness?: number; // Try to make paragraph longer/shorter by this many lines
}
CurveFidelityConfig
interface CurveFidelityConfig {
distanceTolerance?: number; // Max deviation from curve in font units (default: 0.5)
angleTolerance?: number; // Max angle between segments in radians (default: 0.2)
}
GeometryOptimizationOptions
interface GeometryOptimizationOptions {
enabled?: boolean; // Enable geometry optimization (default: true)
areaThreshold?: number; // Min triangle area for Visvalingam-Whyatt (default: 1.0)
colinearThreshold?: number; // Max angle for colinear removal in radians (default: 0.0087)
minSegmentLength?: number; // Min segment length in font units (default: 10)
}
TextGeometryInfo
interface TextGeometryInfo {
geometry: BufferGeometry; // The final Three.js geometry, ready for use in a Mesh
glyphs: GlyphGeometryInfo[]; // Detailed information and bounds for each individual glyph
planeBounds: {
min: { x: number; y: number; z: number };
max: { x: number; y: number; z: number };
};
stats: {
trianglesGenerated: number; // Total triangles in the final mesh
verticesGenerated: number; // Total vertices in the final mesh
pointsRemovedByVisvalingam: number; // Curve points removed by Visvalingam-Whyatt simplification
pointsRemovedByColinear: number; // Redundant points removed from straight lines
originalPointCount: number; // Total curve points before any optimization
};
query(options: TextQueryOptions): TextRange[]; // Method to find ranges of text within the geometry
coloredRanges?: ColoredRange[]; // If `color` option was used, an array of the resolved color ranges
}
The coloredRanges property contains resolved color assignments when the color option was used. This data includes spatial bounds and glyph references, useful for hit detection or analysis without re-querying
TextQueryOptions
interface TextQueryOptions {
byText?: string[]; // Exact text matches
byCharRange?: { start: number; end: number }[]; // Character index ranges
}
TextRange
interface TextRange {
start: number; // Starting character index
end: number; // Ending character index
originalText: string; // The matched text content
bounds: {
min: { x: number; y: number; z: number };
max: { x: number; y: number; z: number };
}[]; // Array of bounding boxes (splits across lines)
glyphs: GlyphGeometryInfo[]; // Glyphs within this range
lineIndices: number[]; // Line numbers this range spans
}
Memory management
three-text manages memory in two ways: a shared glyph cache for all text, and instance-specific resources for each piece of text you create
The shared cache is handled automatically through an LRU (Least Recently Used) policy. The default cache size is 250MB, but you can configure it per text instance. Tessellated glyphs are cached to avoid expensive recomputation when the same characters (or clusters of overlapping characters) appear multiple times
const text = await Text.create({
text: 'Hello world',
font: '/fonts/font.ttf',
size: 72,
maxCacheSizeMB: 1024, // Custom cache size in MB
});
// Check cache performance
const stats = text.getCacheStatistics();
console.log('Cache Statistics:', {
hitRate: stats.hitRate, // Cache hit percentage
memoryUsageMB: stats.memoryUsageMB, // Memory used in MB
uniqueGlyphs: stats.uniqueGlyphs, // Distinct cached glyphs
totalGlyphs: stats.totalGlyphs, // Total glyphs processed
saved: stats.saved // Tessellations avoided
});
Fonts are cached internally and persist for the application lifetime. The glyph geometry cache uses an LRU eviction policy, so memory usage is bounded by maxCacheSizeMB. When a text mesh is no longer needed, dispose of its geometry as you would any Three.js BufferGeometry:
textMesh.geometry.dispose();
Debugging
Enable internal logging by setting a global flag before the library loads:
In a browser environment:
window.THREE_TEXT_LOG = true;
In a Node.js environment:
THREE_TEXT_LOG=true node your-script.js
The library will output timing information for font loading, geometry generation, line breaking, and text shaping operations. Errors and warnings are always visible regardless of the flag
Browser compatibility
The library requires WebAssembly support for HarfBuzz text shaping:
- Chrome 57+
- Firefox 52+
- Safari 11+
- Edge 16+
WOFF font support requires the DecompressionStream API:
- Chrome 80+
- Firefox 113+
- Safari 16.4+
- Edge 80+
WOFF fonts are automatically decompressed to TTF/OTF using the browser’s native decompression with zero bundle cost. For older browsers, use TTF or OTF fonts directly
ES modules (recommended) are supported in:
- Chrome 61+
- Firefox 60+
- Safari 10.1+
- Edge 16+
UMD build is needed for older browsers:
- Chrome < 61
- Firefox < 60
- Safari < 10.1
- Internet Explorer (all versions)
Performance considerations
While three-text runs on all modern browsers, performance varies significantly based on hardware and browser implementation. In testing on an M2 Max with a 120Hz ProMotion display as well as driving an external 5K display:
Chrome provides the best experience
Firefox also delivers great performance but may exhibit less responsive mouse interactions in WebGL contexts due to the way it handles events
Safari for macOS shows reduced performance, which is likely due to the platform’s conservative resource management, particularly around battery life; 120FPS is not acheivable
The library was also tested on a Brightsign 223HD, which took a long time to generate the initial geometry but seemed fine after that
Testing
The library includes a test suite using Vitest that covers core functionality, error handling, layout features, and performance optimizations:
npm test # Run all tests
npm test -- --watch # Watch mode
npm test -- --coverage # Coverage report
Tests use mocked HarfBuzz and tessellation libraries for fast execution without requiring WASM files
Build system
Development
npm run dev # Watch mode with rollup
npm run serve # Start development server for demos
Production
npm run build # Complete build including patterns
Pattern generation
npm run build:patterns # Generate hyphenation patterns for all languages
npm run build:patterns:en-us # Generate only English US patterns (faster for development)
The build:patterns script uses the tex-hyphen git submodule, which must be initialized. The git clone command in the quick start handles this for you
However, if you cloned the repository without the --recurse-submodules flag, you will need to initialize the submodule manually before this script will work:
git submodule update --init --recursive
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
Build outputs
The build generates multiple module formats:
- ESM:
dist/index.js- ES6 modules for modern bundlers - CommonJS:
dist/index.cjs- Node.js compatibility - UMD:
dist/index.umd.js- Universal module for browsers - Types:
dist/index.d.ts- TypeScript declarations
Hyphenation patterns are available in both ESM and UMD formats in dist/patterns/
Acknowledgements
three-text is built on top of HarfBuzz, TeX, and Three.js, and this library would not exist without the authors and communities who contibute to, support, and steward these projects. Thanks to Theo Honohan and Yasi Perera for the advice on graphics
License
three-text was written by Jeremy Tribby (@jpt) and is licensed under the GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later). See the LICENSE file for details
This software includes code from third-party libraries under compatible permissive licenses. For full license details, see the LICENSE_THIRD_PARTY file