Guide

Guide Getting Started Your first scene

Your first scene

Build a working scene from scratch — load a texture, position a sprite, draw it, and animate it across frames.

Intro ~3 min read

What you'll learn

  • load a texture and draw a sprite
  • center a sprite with an anchor
  • animate state each frame with delta time

Before you start

Your first scene

A scene drives one render context. It owns the data you load, the state that updates each frame, and the calls that go to the backend during draw. Most ExoJS work happens inside scenes.

This chapter builds one from scratch: a single sprite, centered on the canvas, rotating at a fixed rate.

A minimal scene

The smallest viable scene has just a draw method. The application calls it every frame; the scene tells the backend what to put on screen:

import { Application, Scene } from '@codexo/exojs';

class HelloScene extends Scene {
    draw(context) {
        context.backend.clear();
    }
}

const app = new Application({ canvas: { width: 800, height: 600 } });
app.start(new HelloScene());

The context.backend.clear() call paints the canvas with the application’s clearColor (default black). Start each frame with clear unless you specifically want the previous frame to remain.

There’s nothing visible yet because the scene doesn’t render anything beyond the clear. Add a sprite next.

Loading and drawing a sprite

Scenes have four lifecycle hooks. You override the ones you need:

  • async load(loader) — declare assets. Resolves before init runs.
  • init(loader) — set up state. The assets you declared during load are available via loader.get(...).
  • update(delta) — per-frame logic.
  • draw(context) — per-frame rendering.

Adding a single sprite means using load to declare a texture, init to construct the sprite, and draw to render it:

examples/getting-started/hello-world.ts
class HelloWorldScene extends Scene {
    private sprite!: Sprite;

    override async load(loader): Promise<void> {
        this.sprite = new Sprite(await loader.load(Texture, 'image/ship-a.png'));
    }

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

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

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

A few things worth pointing out:

  • loader.load(Texture, { bunny: 'image/bunny.png' }) declares one asset under the name bunny. Inside init, loader.get(Texture, 'bunny') returns the loaded texture instance. Names are stable; paths can change later without touching the rest of the code.
  • The default sprite anchor is (0, 0) — the top-left. With setAnchor(0.5), the sprite’s center becomes its pivot point, so setPosition(width / 2, height / 2) places the sprite at the canvas center rather than offset to one corner.

Adding motion

Override update to advance state once per frame. The delta argument carries the elapsed time since the previous frame; multiplying transforms by delta.seconds makes motion frame-rate independent:

update(delta) {
    this.bunny.rotate(120 * delta.seconds);
}

This rotates the sprite at 120 degrees per second regardless of whether the browser ticks at 60 fps, 144 fps, or stutters under load. The rotate call adds to the current rotation; setRotation would replace it.

Where the canvas lives

The app.canvas property is a regular HTMLCanvasElement. In a real page, you place it wherever your layout needs it:

document.getElementById('scene').append(app.canvas);

For the smallest possible test page, appending to document.body is fine. Either way, the canvas needs to be mounted somewhere in the DOM before frames become visible.

The full thing running

Hello World Open in Playground View source
import { Application, Color, Scene, Sprite, Texture } from '@codexo/exojs';

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

// #region guide:first-scene
class HelloWorldScene extends Scene {
    private sprite!: Sprite;

    override async load(loader): Promise<void> {
        this.sprite = new Sprite(await loader.load(Texture, 'image/ship-a.png'));
    }

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

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

    override draw(context): void {
        context.backend.clear();
        context.render(this.sprite);
    }
}
// #endregion guide:first-scene

app.start(new HelloWorldScene());

Where to go next

The pieces shown here — Application, Scene, Loader, Sprite, Texture — each have their own chapter. Runtime walks through the runtime model in order: how applications and scenes fit together, how the scene graph composes drawables, and how the view transform handles cameras and resolutions.

Next Understand the scene lifecycle

See how load, init, update, and draw work together as one repeatable loop.