Application
How the application owns canvas, render backend, resources, input, scene management, and the frame loop.
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— whatcontext.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/heightare the logical canvas dimensions in CSS pixels.- The backing buffer is
width × pixelRatiobyheight × pixelRatio. canvas.style.width / heightis 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— theLoaderused by every scene to register and resolve assets.app.input— theInputManagerfor keyboard, pointer, and gamepad routing.app.scene— the scene stack controller (push/pop/replace).app.tweens— the globalTweenManager.
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
The minimum complete application: construct, start one scene, draw a single sprite.
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.