Guide

GuideGetting StartedYour 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~4 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:

examples/guides/your-first-scene/minimal-scene.ts
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 before init runs.
  • init() — set up state. Assets are available via this.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:

examples/getting-started/hello-world.ts
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 a Texture synchronously — the path is a literal string, so its .png extension tells the loader what type to produce, and there’s no need to import Texture just to ask for one. Nothing declared this path in load; the get() call in init is what kicks off the fetch.
  • Building the Sprite in init means draw only 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. 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 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.

The full thing running

Hello WorldOpen in PlaygroundView source

Preview is paused until you click Play.

import { Application, Color, FixedResolutionCanvasSizing, type RenderingContext, Scene, Sprite } from '@codexo/exojs';

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);
  }
}
const app = new Application({
  scenes: { HelloWorldScene },
  canvas: {
    width: 1280,
    height: 720,
    mount: document.body,
    sizing: new FixedResolutionCanvasSizing(),
  },
  clearColor: Color.black,
  loader: {
    basePath: 'assets/',
  },
});

await app.start(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.

NextUnderstand the scene lifecycle

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