DENSE TEXT INFRASTRUCTURE FOR PIXIJS 8
Render text at
scene scale.
One retained layer for a million labels, compact glyph batches, incremental updates, CJKV and complex-script shaping, and camera-aware culling across WebGL and WebGPU.
- Resident labels
- 1,000,000
- Language path
- CJKV + 7 scripts
- Fixed CPU store
- 72 MiB
- Renderer path
- WebGL 2 + WebGPU
01 / START
Keep the public surface small.
Create labels synchronously, then publish accepted work through one explicit commit boundary. Stable identities keep hot updates compact.
Install
bun add pixi-glyphflow pixi.js pixi-viewportCreate one retained layer
import { Application } from "pixi.js";
import { TextLayer } from "pixi-glyphflow";
const app = new Application();
await app.init({
resizeTo: window,
preference: ["webgpu", "webgl"],
webgl: { preferWebGLVersion: 2 },
});
document.body.appendChild(app.canvas);
const labels = new TextLayer({
renderer: app.renderer,
initialCapacity: 1_000_000,
culling: {
bounds: { x: 0, y: 0, width: 1280, height: 720 },
padding: 32,
},
});
app.stage.addChild(labels);
labels.create({
text: "Shanghai 24 C",
x: 24,
y: 32,
style: { fontFamily: "Inter", fontSize: 18, fill: 0xffffff },
});
await labels.commit();Vite builds use an ES module worker and an ES2022 target for the lazy HarfBuzz pipeline. This site runs that production configuration.
02 / VIEWPORT
Camera work stays camera work.
The binding converts visible viewport corners into layer-local bounds and coalesces drag, inertia, wheel, pinch, zoom, and rotation into one culling commit per frame.
Bind the camera
import { Viewport } from "pixi-viewport";
import { bindViewport } from "pixi-glyphflow/viewport";
const viewport = new Viewport({
screenWidth: app.screen.width,
screenHeight: app.screen.height,
worldWidth: 18_000,
worldHeight: 12_000,
events: app.renderer.events,
});
viewport.drag().decelerate().wheel().pinch();
app.stage.addChild(viewport);
const binding = bindViewport(labels, viewport, { addChild: true });
await binding.whenIdle();Move 100,000 labels
const movingIds = new Float64Array(100_000);
const positions = new Float32Array(200_000);
// Fill identities from createMany() and write packed x/y pairs.
labels.updatePositions(movingIds, positions);
await labels.commit();
console.table({
revision: labels.stats.revision,
visible: labels.stats.visibleLabelCount,
glyphs: labels.stats.submittedGlyphs,
});01 Camera frames preserve label revisions and shaped glyph runs.
02 Packed Float32 coordinates keep the movement path allocation-light.
03 Every binding listener leaves through one idempotent destroy path.
03 / FONTS
Shape the language. Keep the font choice explicit.
Binary fonts travel through HarfBuzz and glyph-ID MSDF generation. This live chain covers CJKV, Arabic, Devanagari, Hebrew, Thai, Greek, Cyrillic, Vietnamese, and emoji.
Register a custom CJKV font
const productFonts = [
["Product CJKV", "/fonts/product-cjkv.ttf"],
["Product Arabic", "/fonts/product-arabic.ttf"],
["Product Devanagari", "/fonts/product-devanagari.ttf"],
["Product Hebrew", "/fonts/product-hebrew.ttf"],
["Product Thai", "/fonts/product-thai.ttf"],
] as const;
await Promise.all(
productFonts.map(async ([family, url]) => {
const source = new Uint8Array(
await fetch(url).then((response) => response.arrayBuffer()),
);
await labels.fonts.register({ family, source });
}),
);
labels.fonts.registerFallback("global-ui", [
...productFonts.map(([family]) => family),
"system-ui",
"PingFang SC",
"Hiragino Sans",
"Apple SD Gothic Neo",
"sans-serif",
]);
labels.create({
text: "日本語 · 東京テキスト",
style: { fontFamily: "global-ui", fontSize: 24 },
shaping: {
language: "ja",
script: "Jpan",
features: ["kern", "liga"],
variations: { wght: 560 },
},
});
await labels.commit();01 Language and script tags select localized OpenType glyphs.
02 Glyph ID coverage advances through custom binary families in order.
03 CSS family stacks reach PixiJS layout and Canvas rasterization intact.
04 / PERFORMANCE
Measured under pressure.
Committed Chrome and WebGL 2 artifacts use isolated processes, GPU completion, warmup frames, and p95 reporting on an Apple M1 Pro.
| Workload | Scale | Frame p95 |
|---|---|---|
| Million-label viewport | 1,000,000 resident | 5.20 ms |
| Viewport drag + inertia | 1,000,000 resident | 5.40 ms |
| Wheel + pinch zoom | 1,000,000 resident | 7.10 ms |
| Position storm | 100,000 packed moves | 9.00 ms |
| Dynamic counters | 100,000 text + position | 14.80 ms |
05 / ARCHITECTURE
One revision, four bounded stages.
Each stage owns a deep implementation boundary. The application learns one label model and one commit lifecycle.
- 01StoreDense identities + dirty journal
- 02ShapeLayout + HarfBuzz worker
- 03ResidentAtlas + glyph instances
- 04SubmitWebGL / WebGPU adapter
06 / API
Focused entry points.
Core usage stays on the root import. Optional integrations remain isolated so applications carry the surfaces they use.
Package paths
pixi-glyphflow- TextLayer, FontRegistry, and primary types
pixi-glyphflow/viewport- Frame-coalesced pixi-viewport binding
pixi-glyphflow/shaping- HarfBuzz main-thread and worker shapers
pixi-glyphflow/accessibility- Sparse semantic DOM mirror
pixi-glyphflow/advanced- Atlas, mesh, layout, upload, and spatial primitives
TextLayer essentials
createMany(specs)- Create a validated batch and return stable TextIds.
updatePositions(ids, xy)- Apply packed position changes in one columnar pass.
updateTextPositions(ids, text, xy)- Broadcast dynamic text with packed positions.
showAll() / hideAll()- Toggle every resident label through one columnar mutation.
commit()- Publish one monotonic revision through render and culling work.
setViewportBounds(bounds)- Select the resident subset submitted to the renderer.
stats- Read immutable capacity, culling, upload, draw, and timing diagnostics.
07 / GUIDES
Follow the operating path.
The maintained Markdown set carries complete contracts, compatibility boundaries, benchmark evidence, and migration detail.