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'.
clearColor is not just a construction-time default — app.clearColor returns the render
backend’s live Color instance, so it stays readable and writable for the
whole life of the application. Assigning a new Color copies its channels into the backend
(effective from the next frame), which makes it a convenient way to react to game state without
touching draw code:
import { Application, Color } from '@codexo/exojs';
const app = new Application();
function onLowHealth(isLow) {
app.clearColor = isLow ? Color.darkRed : Color.cornflowerBlue;
}
You can also mutate the existing instance in place instead of allocating a new Color each time:
app.clearColor.set(20, 20, 40, 1); // dusk
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
canvas.pixelRatio scales the backing buffer relative to logical CSS pixels. By default it is the host’s window.devicePixelRatio clamped to 2, so rendering is crisp on Retina/HiDPI screens out of the box. Pass an explicit value to override:
const app = new Application({
canvas: {
width: 1280,
height: 720,
pixelRatio: window.devicePixelRatio, // full native density (uncapped)
},
});
width/heightare the logical canvas dimensions in CSS pixels — also exposed asapp.width/app.height.- The backing buffer is
width × pixelRatiobyheight × pixelRatio;app.pixelRatioholds the resolved ratio. canvas.style.width / heightis always set to the logical dimensions.app.resize(w, h)takes logical dimensions and re-applies the stored pixel ratio.
Pass pixelRatio: 1 to force logical-pixel rendering, or window.devicePixelRatio to opt into full native density above the default 2× cap (the cap keeps DPR-3 phones from paying a 9× fill-rate cost).
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— theSceneManagerthat holds the single active scene; switch scenes viasetScene(with optional fade).app.audio— theAudioManagerfor playback, busses, and the spatial listener.app.systems— theSystemRegistryfor app-scopedSysteminstances.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.