Guide

Guide Rendering Text

Text

Control layout, styling, and visual effects for runtime text.

Intro ~4 min read

What you'll learn

  • render and style runtime text
  • lay out multiline and wrapped text

Text

Text renders GPU-accelerated text strings as nodes in the scene graph. Each Text instance rasterizes its glyphs into the engine’s shared glyph atlas and draws them as a single Mesh — one draw call per text node, regardless of string length. The node extends Container and inherits full transform, filter, blend, and mask support.

A minimal text node

import { Color, Text } from '@codexo/exojs';

const label = new Text('Hello', {
    fillColor: Color.white,
    fontSize: 24,
    fontFamily: 'Arial',
});

The second argument is a TextStyleOptions object — all properties are optional, all have defaults. The TextStyle is stored as text.style. Assign a new style to rebuild the mesh; mutating style fields alone does not rebuild:

import { Color, TextStyle } from '@codexo/exojs';

label.style.fontSize = 32;
label.style.fillColor = Color.tomato;
label.style = label.style; // apply style changes and rebuild

Font loading

System fonts (Arial, Times New Roman, etc.) work immediately. For custom web fonts, load a FontFace in the scene’s load hook:

async load(loader) {
    await loader.load(FontFace, {
        myFont: 'font/MyFont.woff2',
    }, { family: 'MyFont' });
}

init(loader) {
    this.title = new Text('Custom Font', {
        fontFamily: 'MyFont',
        fontSize: 48,
        fill: 'white',
    });
}

The FontFace asset is registered with the document’s document.fonts set. Once loaded, it becomes available to all Text instances via the fontFamily style property.

Style properties

TextStyle exposes these configurable properties:

PropertyTypeDefaultDescription
fontFamilystring'Arial'CSS font family name
fontSizenumber20Font size in pixels
fontWeightstring'bold'CSS font weight
fontStyle'normal' | 'italic''normal'Font style
fillstring | CanvasGradient | CanvasPattern'black'Stored style property (not currently applied by the renderer)
fillColorColorColor.whiteTint used when building the text mesh
strokestring | CanvasGradient | CanvasPattern'black'Stored style property (not currently applied by the renderer)
strokeThicknessnumber1Stored style property (not currently applied by the renderer)
align'left' | 'center' | 'right''left'Text alignment
lineHeightnumber1.2Multiplier on fontSize for vertical spacing
wordWrapbooleanfalseStyle flag (currently not applied by the layout engine)
wordWrapWidthnumber100Style width hint (currently not applied by the layout engine)
baselineCanvasTextBaseline'alphabetic'Stored style property (not currently applied by the renderer)
lineJoinCanvasLineJoin'miter'Stored style property (not currently applied by the renderer)
miterLimitnumber10Stored style property (not currently applied by the renderer)
paddingnumber0Stored style property (not currently applied by the renderer)

Current renderer path applies fontFamily, fontSize, fontWeight, fontStyle, align, lineHeight, and fillColor. Other TextStyle fields are currently stored but not applied.

Glyphs are rasterized in white, and the mesh tint is initialized from fillColor when the mesh is rebuilt.

Multiline text and wrap settings

Multiline text works by embedding \n characters in the string:

const dialog = new Text('Line one\nLine two\nLine three', {
    fill: 'white',
    fontSize: 18,
    lineHeight: 1.5,
});

wordWrap and wordWrapWidth are available on TextStyle, but the current text layout only applies explicit line breaks (\n) and alignment:

const wrapped = new Text(longString, {
    fill: 'white',
    fontSize: 16,
    wordWrap: true,
    wordWrapWidth: 400,
});

Alignment

The align property controls horizontal positioning relative to the Text node’s local origin:

const centered = new Text('Centered Title', {
    align: 'center',
    fill: 'white',
    fontSize: 32,
});
centered.setPosition(canvasWidth / 2, 20);

A center-aligned text node at (400, 20) centers its glyphs horizontally around x=400 in local space.

Text in the scene graph

Text extends Container, so it carries position, rotation, scale, origin, and anchor. You can rotate text, tint the whole node, apply filters, and nest it inside other containers:

this.ui = new Container();
this.scoreLabel = new Text('Score: 0', { fill: 'white', fontSize: 24 });
this.scoreLabel.setPosition(10, 10);
this.ui.addChild(this.scoreLabel);
this.addChild(this.ui);

The text node’s internal mesh is a Container child. Assigning to text.text destroys the previous mesh and builds a new one. This means:

  • Changing text every frame is fine — the old mesh is discarded and a fresh one is built.
  • Mutating style fields alone does not rebuild the mesh.
  • To apply style changes, assign text.style = ... so the mesh is rebuilt.

Text constraints

  • The glyph atlas is shared across all Text instances and has a fixed default size. Large sets of unique glyph/style combinations can exhaust atlas space.
  • Text does not expose per-character styling. Use separate Text instances for mixed-style strings.
  • Text does not measure or report its pixel dimensions through the public API — the mesh bounds reflect the glyph quad size but measured width/height in pixels is not exposed as a public getter.
  • Loader-based FontFace loading is the built-in path for custom fonts; any font family available to the browser’s canvas text engine can be used once loaded.

Examples

Basic Text Open in Playground View source
import { Application, Color, FontAsset, Scene, Text, Time } from '@codexo/exojs';

const app = new Application({
    canvas: {
        width: 1280,
        height: 720,
        mount: document.body,
        sizingMode: 'fit',
    },
    clearColor: Color.black,
    loader: {
        basePath: 'assets/',
    },
});

class BasicTextScene extends Scene {
    private time!: Time;
    private text!: Text;

    override async load(loader): Promise<void> {
        await loader.load(FontAsset, { example: 'font/Kenney Future.ttf' }, { family: 'Kenney Future' });
    }

    override init(): void {
        const { width, height } = this.app.canvas;

        this.time = new Time();

        this.text = new Text('Hello World!', {
            align: 'left',
            fillColor: Color.white,
            outlineColor: Color.black,
            outlineWidth: 0.2,
            fontSize: 25,
            fontFamily: 'Kenney Future',
        });

        this.text.setPosition(width / 2, height / 2);
        this.text.setAnchor(0.5, 0.5);
    }

    override update(delta): void {
        this.text.text = `Hello World! ${this.time.addTime(delta).seconds | 0}`;
        this.text.rotate(delta.seconds * 36);
    }

    override draw(context): void {
        context.backend.clear();
        context.render(this.text);
    }
}

app.start(new BasicTextScene());

A text node with a custom web font, updating its string each frame.

Multiline and Wrap Open in Playground View source
import { Application, Color, Scene, Text } from '@codexo/exojs';

const app = new Application({
    canvas: {
        width: 1280,
        height: 720,
        mount: document.body,
        sizingMode: 'fit',
    },
    clearColor: Color.black,
});

const paragraph = 'ExoJS text layout can render multiline content with configurable wrapping behavior and style.';
const longToken = 'ExoJStextlayoutrendersaverylongunbrokentokenwithoutanyspacestobreakon';

const titleColor = new Color(140, 170, 210);

class MultilineAndWrapScene extends Scene {
    private titleA!: Text;
    private textA!: Text;
    private titleB!: Text;
    private textB!: Text;
    private titleC!: Text;
    private textC!: Text;

    override init(): void {
        const { width, height } = this.app.canvas;

        // Three wrap modes side by side across the 16:9 canvas: one column each.
        const colWidth = width / 3;
        const titleY = height * 0.16;
        const bodyY = height * 0.16 + 36;
        const colX = (index: number): number => colWidth * index + (colWidth - 360) / 2;

        this.titleA = new Text('No wrap — single line overflows', { fillColor: titleColor, fontSize: 16 });
        this.titleA.setPosition(colX(0), titleY);
        this.textA = new Text(paragraph, { fillColor: Color.white, fontSize: 22 });
        this.textA.setPosition(colX(0), bodyY);

        this.titleB = new Text('Word wrap @ 360px — at word boundaries', { fillColor: titleColor, fontSize: 16 });
        this.titleB.setPosition(colX(1), titleY);
        this.textB = new Text(paragraph, { fillColor: Color.white, fontSize: 22 }, { maxWidth: 360 });
        this.textB.setPosition(colX(1), bodyY);

        this.titleC = new Text('Break words @ 280px — splits a token', { fillColor: titleColor, fontSize: 16 });
        this.titleC.setPosition(colX(2), titleY);
        this.textC = new Text(longToken, { fillColor: Color.white, fontSize: 22 }, { maxWidth: 280, breakWords: true });
        this.textC.setPosition(colX(2), bodyY);
    }

    override draw(context): void {
        context.backend.clear();
        context.render(this.titleA);
        context.render(this.titleB);
        context.render(this.titleC);
        context.render(this.textA);
        context.render(this.textB);
        context.render(this.textC);
    }
}

app.start(new MultilineAndWrapScene());

Multiline text with alignment and line-height control.

Where to go next

The next chapter, Animation, covers frame-based sprite animation, tweens for interpolated motion, and how to combine manual frame-loop updates with automated tween-driven timing.