Your first scene
Build a working scene from scratch — load a texture, position a sprite, draw it, and animate it across frames.
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 beforeinitruns.init(loader)— set up state. The assets you declared duringloadare available vialoader.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:
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 namebunny. Insideinit,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. WithsetAnchor(0.5), the sprite’s center becomes its pivot point, sosetPosition(width / 2, height / 2)places the sprite at the canvas center rather than offset to one corner.
A sprite that won't center
A sprite’s default anchor is its top-left corner. setPosition(width / 2, height / 2) then places the corner — not the center — at the middle of the canvas. Call setAnchor(0.5) first.
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
Try it
Playground
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.