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 defaulttext.mergeStyle({ fill: 0xff0000 }); // patches: keeps the current font/sizeWhen to reach for UI instead
Section titled “When to reach for UI instead”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 / backdrop →
UIPanel+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.
Wrapper
Section titled “Wrapper”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.
Pixel-art text (bitmap)
Section titled “Pixel-art text (bitmap)”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.
Synthetic bold and italic on bitmap fonts
Section titled “Synthetic bold and italic on bitmap fonts”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.
Loading a web font (canvas Text)
Section titled “Loading a web font (canvas Text)”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.
Engine-level default text style
Section titled “Engine-level default text style”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 style → UIPlugin 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.
The Pixi v8 resolution gotcha
Section titled “The Pixi v8 resolution gotcha”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.