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:
class HelloScene extends Scene {
override draw(context: RenderingContext): void {
context.render(this.root);
}
}
const app = new Application({ scenes: { HelloScene }, canvas: { width: 800, height: 600 } });
await app.start(HelloScene);The canvas is already painted with the application’s clearColor before draw runs — the engine clears every frame, so a scene never has to open with a clear of its own. Pass autoClear: false to Application if you want the previous frame to remain instead.
There’s nothing visible yet because the scene’s root is still empty. Add a sprite next.
Loading and drawing a sprite
Scenes have four lifecycle hooks. You override the ones you need:
async load()— declare assets upfront. Resolves beforeinitruns.init()— set up state. Assets are available viathis.loader.get(...).update(delta)— per-frame logic.draw(context)— per-frame rendering.
This scene skips load entirely. Texture is a seamless asset type: this.loader.get(...) never throws, even for a path nothing has fetched yet — it hands back a placeholder immediately and heals it in place once the network request resolves. That’s enough to build and position a sprite directly in init:
class HelloWorldScene extends Scene {
private sprite!: Sprite;
override init(): void {
const app = this.app;
const { width, height } = app;
this.sprite = new Sprite(this.loader.get('image/ship-a.png'));
this.sprite.setAnchor(0.5);
this.sprite.setPosition(width / 2, height / 2);
}
override draw(context: RenderingContext): void {
context.render(this.sprite);
}
}A few things worth pointing out:
this.loader.get('image/ship-a.png')returns aTexturesynchronously — the path is a literal string, so its.pngextension tells the loader what type to produce, and there’s no need to importTexturejust to ask for one. Nothing declared this path inload; theget()call ininitis what kicks off the fetch.- Building the
Spriteininitmeansdrawonly ever reads state — it never creates it. If the texture is still loading when the first frame renders, the sprite shows a “missing” placeholder that heals in place once the fetch resolves; see Loading and resources for the full status-channel contract. - 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.
Prefer load() once a scene has more than a placeholder
Getting away without load works because Texture heals in place — fine for a one-sprite demo. Once a scene has several assets, or you don’t want a visible pop-in while the texture arrives, declare them upfront with async load() { await this.loader.load(...); } instead — see Loading and resources.
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 makes motion frame-rate independent:
update(delta: Seconds) {
this.sprite.rotate(120 * delta);
}
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 canvas.mount option puts the canvas wherever your layout needs it:
import { Application } from '@codexo/exojs';
const mount = document.getElementById('scene');
if (mount === null) throw new Error('Missing #scene mount element.');
const app = new Application({ canvas: { mount } });
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.
A canvas outside the DOM shows nothing
If the canvas is never appended to the page, the scene still loads, updates, and draws — you just see a blank page with no error to explain why. Mount it before you expect a frame.
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.


