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 / EXAMPLES
Add the state each label needs.
Every label starts with text. Position, visibility, groups, layout, shaping, and style remain optional layers that can arrive during creation or later updates.
Create with one required field
const id = labels.create({
text: "Hello, Glyphflow",
});
await labels.commit();Coordinates default to the origin, visibility defaults to true, and the renderer uses its standard text style.
Hide a separately created group or one TextId
const stationSigns = labels.createGroup();
labels.create({ text: "Entrance", group: stationSigns });
const exit = labels.create({ text: "Exit", group: stationSigns });
labels.setGroupVisible(stationSigns, false);
await labels.commit();
labels.setGroupVisible(stationSigns, true);
labels.update(exit, { visible: false });
await labels.commit();Group masks preserve each member's local visible flag. Every createGroup call returns a fresh identity owned by the layer.
Opt into vertical flow and appearance
labels.create({
text: "入口",
x: 80,
y: 40,
layout: { writingMode: "vertical-rl" },
style: {
fontSize: 24,
fontWeight: "700",
fill: 0x38bdf8,
},
});
await labels.commit();Vertical labels stack upright glyphs from top to bottom. Explicit lines form columns from right to left.
03 / 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.
04 / 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.
05 / 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 |
06 / 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
07 / 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
create(spec)- Create a label; text is the required field.
createMany(specs)- Create a validated batch and return stable TextIds.
createGroup()- Create one unique layer-local TextGroupId.
update(id, patch)- Change selected fields for one stable identity.
setGroupVisible(group, visible)- Apply one mask while retaining label-local visibility.
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.
08 / GUIDES
Follow the operating path.
The maintained Markdown set carries complete contracts, compatibility boundaries, benchmark evidence, and migration detail.