Skip to content

Text

TextComponent is for free-positioned text on a render layer — a debug HUD overlay, a diegetic floor label, a one-off world-space string that should zoom and rotate with the camera. It’s Transform-synced like a sprite, serializable, and takes a style object that mirrors CSS font properties.

import { TextComponent } from "@yagejs/renderer";
entity.add(new Transform());
entity.add(
new TextComponent({
text: "DEBUG",
layer: "debug",
anchor: { x: 0.5, y: 0 },
style: {
fontFamily: "ui-monospace, monospace",
fontSize: 14,
fill: 0xf8fafc,
fontWeight: "bold",
},
}),
);

Update at runtime with setText() / setStyle(), or tweak tint and alpha directly. setStyle() replaces the style — properties you don’t pass fall back to the defaults. To change a few properties while keeping the rest, use mergeStyle():

text.setStyle({ fill: 0xff0000 }); // replaces: font/size revert to default
text.mergeStyle({ fill: 0xff0000 }); // patches: keeps the current font/size

For most “text on screen” cases — HUD cards, score readouts with a backdrop, dialog boxes, nameplates above an enemy, floating damage numbers that should stay axis-aligned at any zoom — UIPanel + UIText from @yagejs/ui is the right answer. You get flexbox layout (padding, gap, alignment), a styled background fill, and child stacking, all of which TextComponent deliberately doesn’t try to provide.

Specifically:

  • Text widget with layout / padding / backdropUIPanel + UIText.
  • Entity-tracked text that stays constant-size at any zoom (nameplates, damage numbers, interaction prompts) → ScreenFollow + UIPanel({ positioning: "transform" }).
  • Free string with no layout, that should follow camera transforms (diegetic floor label, debug overlay, world-pinned billboard text) → TextComponent.

Thin wrapper: the style option forwards as-is to pixi’s TextStyleOptions — full set of CSS-style font properties available (fontFamily, fontSize, fontWeight, fontStyle, fill, letterSpacing, lineHeight, dropShadow, etc.). .text is the underlying pixi Text instance (or BitmapText when bitmap is set). See the pixi Text guide and TextStyle reference for every available option.

Canvas-rasterised Text is bilinear-sampled by the GPU, so it goes blurry the moment it’s drawn at a non-integer scale — a zoomed pixel-art camera, a 4× upscale — on a non-Retina display. BitmapText draws pre-baked glyph quads instead, so it stays crisp. Set the bitmap option (works on TextComponent and UIText alike):

Set bitmap: true and Pixi bakes (or looks up) the glyph atlas from style.fontFamily at style.fontSize — the font lives in style like every other property:

// Zero-config: bake a dynamic bitmap font from this text's own style.
new TextComponent({
text: "SCORE",
bitmap: true,
style: { fontFamily: "monospace", fontSize: 12 },
});
// Render with an installed / loaded bitmap font: name it via fontFamily.
new TextComponent({
text: "READY",
bitmap: true,
style: { fontFamily: "PressStart", fontSize: 16 },
});

To bake a bitmap atlas from a .ttf at runtime, use installBitmapFont — it loads the font and returns a name you hand to style.fontFamily:

import { installBitmapFont, TextComponent } from "@yagejs/renderer";
const font = await installBitmapFont("fonts/PressStart2P.ttf", {
name: "PressStart",
size: 16, // glyph bake size (default 32)
resolution: 2, // keeps glyphs crisp when upscaled (default 2)
});
entity.add(
new TextComponent({ text: "READY", bitmap: true, style: { fontFamily: font } }),
);

Glyphs bake white by default so a per-text fill / tint (multiplied over the atlas) can recolour them — adding a gold fill to the style renders gold. Pass a style.fill to installBitmapFont only when you want to bake a fixed colour into the atlas instead. Recolour at runtime with mergeStyle({ fill }) so the fontFamily is preserved — a plain setStyle({ fill }) replaces the style and drops the font.

When a font is no longer rendered, free its atlas (and every variant) plus the source face with uninstallBitmapFont(name) — the symmetric counterpart of installBitmapFont. Baked bitmap fonts are reference-counted by family name, so a family shared by an installBitmapFont and a webFont({ bitmap }) (or by two web-font loads) is destroyed only once the last owner releases it — uninstallBitmapFont and web-font unload are safe to interleave. Pointing one family name at two different source fonts still collides in Pixi’s global registry (the last bake wins), so keep family names unique.

For a pre-built BMFont (.fnt/.xml + atlas), preload the bitmapFont("fonts/x.fnt") handle through a scene’s preload and pass the fontFamily declared in the descriptor as style.fontFamily. Bitmap text honours the same Yoga word-wrap / truncate behaviour as canvas text inside a UIPanel.

Canvas Text synthesises bold and italic from style.fontWeight / fontStyle, but a plain BitmapText has only the one atlas it was baked from, so those props are silently ignored. To get emphasis on bitmap text, bake the variants up front with installBitmapFont’s variants option — each entry produces a sibling atlas from the same .ttf:

await installBitmapFont("fonts/Body.ttf", {
name: "Body",
variants: [
{ fontWeight: "bold" },
{ fontStyle: "italic" },
{ fontWeight: "bold", fontStyle: "italic" },
],
});
// Asks for the bold atlas — selected automatically by fontWeight:
entity.add(
new TextComponent({
text: "GAME OVER",
bitmap: true,
style: { fontFamily: "Body", fontWeight: "bold" },
}),
);

You never name or look up the variant atlases yourself — set fontWeight / fontStyle on the BitmapText exactly as you would for canvas text and the matching atlas is resolved by fontFamily. A fontWeight of "bold"/"bolder" or numeric "600"+ lands on the bold axis; "italic"/"oblique" on the slant axis. A request with no matching variant (or a font baked without variants) falls back to the base atlas, so regular text is unaffected.

Each BitmapFontVariant is { fontWeight?, fontStyle?, style? } — the optional per-variant style layers extra TextStyle props onto that atlas alone.

For plain canvas text in a custom font, use webFont — the canvas sibling of bitmapFont. Preload the handle so the face registers before the first draw; Pixi caches fallback metrics on first paint, so a font that loads late never applies.

import { webFont, TextComponent, Scene } from "@yagejs/renderer";
class MenuScene extends Scene {
readonly preload = [webFont("fonts/Inter.woff2", { family: "Inter" })];
onEnter() {
this.spawn(/* … */).add(
new TextComponent({ text: "Play", style: { fontFamily: "Inter" } }),
);
}
}

The family you pass is what you set as style.fontFamily; omit it to let Pixi derive the family from the file name.

One font, both canvas and bitmap (webFont({ bitmap }))

Section titled “One font, both canvas and bitmap (webFont({ bitmap }))”

A webFont can bake a BitmapText atlas from the same loaded face, so a single declared font is usable both as canvas Text and as a bitmap atlas under one family — no separate installBitmapFont call, no second name to keep in sync. Pass bitmap to the handle:

import { webFont, TextComponent, Scene } from "@yagejs/renderer";
class HudScene extends Scene {
readonly preload = [
// `bitmap: true` bakes with defaults; pass an object to tune the bake.
webFont("fonts/Inter.woff2", {
family: "Inter",
bitmap: { size: 24, variants: [{ fontWeight: "bold" }] },
}),
];
onEnter() {
// Crisp UI prose, GPU-rasterised canvas Text:
this.spawn(/* … */).add(
new TextComponent({ text: "Settings", style: { fontFamily: "Inter" } }),
);
// Same family, drawn from the baked atlas — stays sharp under camera zoom:
this.spawn(/* … */).add(
new TextComponent({
text: "SCORE 9000",
bitmap: true,
style: { fontFamily: "Inter", fontWeight: "bold" },
}),
);
}
}

The canvas face and the baked atlas live in separate Pixi registries, so sharing the one family between them never collides. bitmap accepts true (bake with defaults) or a WebFontBakeOptions object — { size?, chars?, resolution?, padding?, style?, variants? }, the same knobs as installBitmapFont minus name/family (the atlas always registers under the web font’s family). Emphasis variants work exactly as they do for installBitmapFont.

bitmap requires an explicit family: the baked atlas needs a stable name to register under and to uninstall when the scene unloads, so the bake is skipped (with a console warning) when family is omitted. Unloading the web font drops its canvas face and releases its hold on the baked atlas, which is destroyed once every owner sharing the family has released it — so two scenes preloading the same webFont, or a webFont that shares a family with an installBitmapFont, never tear the atlas out from under each other.

To set an app-wide font / fill without importing pixi to poke TextStyle.defaultTextStyle, pass defaultTextStyle to RendererPlugin — it’s the base under every TextComponent / UIText style, and per-text values still win:

new RendererPlugin({
width: 800,
height: 600,
defaultTextStyle: { fontFamily: "Inter", fill: 0xf8fafc },
});

UIPlugin({ defaultTextStyle }) layers a UI-only override on top, so widgets can use a different font than free-positioned TextComponent. Precedence: per-text styleUIPlugin default → RendererPlugin default → pixi default. The default re-applies on setStyle, so a recolour keeps it.

Animated / per-glyph text (SplitTextComponent)

Section titled “Animated / per-glyph text (SplitTextComponent)”

For typewriter reveals, per-letter colour / wave, or staggered line entrances, reach for SplitTextComponent — it wraps Pixi v8’s experimental SplitText / SplitBitmapText and exposes the text as arrays of individually transformable display objects: chars, words, and lines. Like TextComponent it’s Transform-synced and lives on a render layer; set bitmap to split a bitmap font instead of canvas text.

import { SplitTextComponent } from "@yagejs/renderer";
import { Tween, ProcessComponent } from "@yagejs/core";
entity.add(new Transform());
const title = entity.add(
new SplitTextComponent({
text: "GAME OVER",
style: { fontFamily: "monospace", fontSize: 48, fill: 0xffffff },
charAnchor: 0.5, // rotate / scale each glyph about its center
}),
);
// Typewriter reveal — stagger each glyph's fade-in by 50ms, ticked by a
// ProcessComponent (the renderer-side equivalent of useSplitText's `run`).
title.chars.forEach((char) => (char.alpha = 0));
const pc = entity.add(new ProcessComponent());
Tween.stagger(title.chars, (char) => Tween.to(char, "alpha", 1, 300), 50).forEach((p) =>
pc.run(p),
);
// Per-letter sine wave (in a component update)
title.chars.forEach((char, i) => {
char.y = Math.sin(this.t * 4 + i * 0.4) * 6;
});

The transform origins — charAnchor, wordAnchor, lineAnchor (normalized 0–1, settable at construction or at runtime) — control the pivot each segment rotates and scales about. chars are Text / BitmapText; words and lines are containers grouping them.

In Pixi v8, resolution is a Text constructor option, not a TextStyle property. Setting TextStyle.defaultTextStyle.resolution does nothing — text stays rasterised at resolution 1 and blurs when scaled. There’s no global toggle; pass resolution per text:

new TextComponent({ text: "HUD", resolution: window.devicePixelRatio });

resolution is ignored when bitmap is set — a bitmap font’s resolution is fixed when its atlas is baked (installBitmapFont({ resolution })). For genuinely pixel-perfect text at any zoom, prefer bitmap over chasing canvas resolution.