Guide

Guide Runtime Application

Application

How the application owns canvas, render backend, resources, input, scene management, and the frame loop.

Intro ~4 min read

What you'll learn

  • create and configure an Application
  • understand how the application owns canvas, sizing, and the frame loop

Before you start

Application

The Application is the runtime host. One instance per browser tab owns the canvas, the render backend, the asset loader, the input manager, the scene manager, the tween manager, and the per-frame loop. Everything else in your project plugs into it.

Construction

Every option is optional. If you pass nothing, ExoJS picks reasonable defaults — an 800×600 canvas it created itself, a cornflowerBlue clear color, and an auto-selected render backend (WebGPU when available, WebGL2 otherwise).

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

const app = new Application();

When you need to control the configuration, pass a grouped options object:

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

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

Options are organised into groups:

  • canvas — canvas element, size, pixel ratio, and rendering hints (CanvasApplicationOptions).
  • loader — base path, fetch options, caching (LoaderOptions).
  • rendering — WebGL2 debug, context attributes, batch sizes (RenderingApplicationOptions).
  • input — gamepad definitions, slot strategy, pointer threshold (InputApplicationOptions).
  • clearColor — what context.backend.clear() paints each frame.
  • backend{ type: 'auto' } (default), or pin to 'webgl2' / 'webgpu'.

Choosing the canvas

By default the application creates its own canvas. Read it from app.canvas and place it in your page wherever your layout needs it:

const app = new Application({
    canvas: { width: 800, height: 600 },
});
document.body.append(app.canvas);

If your page already has a canvas — for example because the layout is owned by a framework that renders the element for you — pass it in via canvas.element:

const canvas = document.querySelector('canvas');
const app = new Application({
    canvas: { element: canvas, width: 800, height: 600 },
});

Either way, app.canvas is the active HTMLCanvasElement. CSS rules, ResizeObserver, event listeners, and getBoundingClientRect() all work as expected.

High-DPI and pixel ratio

Pass canvas.pixelRatio to scale the backing buffer relative to logical CSS pixels. A value of window.devicePixelRatio produces crisp rendering on high-DPI screens:

const app = new Application({
    canvas: {
        width: 1280,
        height: 720,
        pixelRatio: window.devicePixelRatio,
    },
});
  • width / height are the logical canvas dimensions in CSS pixels.
  • The backing buffer is width × pixelRatio by height × pixelRatio.
  • canvas.style.width / height is always set to the logical dimensions.
  • app.resize(w, h) takes logical dimensions and re-applies the stored pixel ratio.

Default pixelRatio is 1 — no scaling — which preserves the old behaviour.

For pixel-art games where you want crisp upscaling without browser blurring, combine pixelRatio with the imageRendering hint:

const app = new Application({
    canvas: {
        width: 320,
        height: 240,
        imageRendering: 'pixelated',
    },
});

imageRendering is a CSS hint on the canvas element that controls how the browser upscales the canvas in the page. It does not change engine texture filtering.

Starting the frame loop

The frame loop only runs once you give the application a scene to run:

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

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

const app = new Application();
app.start(new HelloScene());

The app.start(scene) call is asynchronous — it initializes the render backend, runs the scene’s load hook, then init, then drives the frame loop until you stop it. You normally don’t need to await the returned promise; fire-and-forget is the common pattern.

To stop the loop, call app.stop(). To release the canvas and all GPU resources, call app.destroy().

Subsystems on the application

Once an application exists, its subsystems are accessible as properties:

  • app.canvas — the render target.
  • app.loader — the Loader used by every scene to register and resolve assets.
  • app.input — the InputManager for keyboard, pointer, and gamepad routing.
  • app.scene — the scene stack controller (push/pop/replace).
  • app.tweens — the global TweenManager.

Most code reaches these through the active scene (this.app.input, this.app.tweens) rather than holding a top-level reference, but both work.

Per-frame signals

The application emits a small set of signals you can subscribe to without subclassing:

app.onFrame.add(time => {
    console.log(`Frame at ${time.seconds.toFixed(3)}s`);
});

app.onResize.add((width, height) => {
    // canvas resolution changed
});

For most projects the scene’s update and draw hooks are enough; reach for these signals when you need to coordinate work that lives outside the scene.

Pausing while the tab is hidden

Browsers throttle background tabs to roughly 1 fps. For games this usually shows up as motion artifacts when the user comes back. Toggle pauseOnHidden to skip update + render entirely while the tab is hidden:

app.pauseOnHidden = true;

Tools and background-active simulations should leave it off; games and animated demos should turn it on.

Examples

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());

The minimum complete application: construct, start one scene, draw a single sprite.

Resize and DPR Open in Playground View source
import { Application, Color, Scene, Sprite, Text, Texture } from '@codexo/exojs';

// #region guide:app-setup
const app = new Application({
    canvas: {
        width: 1280,
        height: 720,
        mount: document.body,
        sizingMode: 'fit',
        pixelRatio: window.devicePixelRatio || 1,
    },
    clearColor: Color.black,
    loader: {
        basePath: 'assets/',
    },
});
// #endregion guide:app-setup

document.body.style.margin = '0';

// #region guide:resize
// This example demonstrates manual resize handling: the canvas is resized to
// fill the window on every `resize` event. (For a hands-off alternative, enable
// the `autosize` canvas option instead.)
window.addEventListener('resize', () => {
    app.resize(window.innerWidth, window.innerHeight);
});

app.resize(window.innerWidth, window.innerHeight);
// #endregion guide:resize

class ResizeScene extends Scene {
    private sprite!: Sprite;
    private info!: Text;

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

    override init(): void {
        this.sprite.setAnchor(0.5);

        this.info = new Text('', { fillColor: Color.white, fontSize: 16 });
        this.info.setAnchor(0.5, 0);

        this.layout();
    }

    override update(): void {
        this.layout();
    }

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

    // #region guide:layout
    private layout(): void {
        const { width, height } = this.app.canvas;
        const dpr = Math.max(1, window.devicePixelRatio || 1);

        this.sprite.setPosition(width / 2, height / 2);
        this.info.setPosition(width / 2, 12);
        this.info.text = `${width}x${height} @ DPR ${dpr.toFixed(2)}`;
    }
    // #endregion guide:layout
}

app.start(new ResizeScene());

Same shape, with the canvas auto-resizing to the window and rendering at the device’s pixel ratio for sharp output on high-DPI displays.

Where to go next

Scenes are where actual game logic lives. The next chapter, Scenes & lifecycle, covers what a scene is, how it differs from the application, and the order its hooks run in.