API reference

Every public class, method, and event in @codexo/exojs. Generated from source.

C

classApplication

@codexo/exojs / core / stable

Top-level engine instance. Owns the canvas, render backend, scene-stack controller, the core systems (input, interaction, audio, coroutines, tweens, animations, rendering), the app-level SystemRegistry for user/extension systems, asset loader, and the per-frame loop. Lifecycle: construct with options → `await app.start(scene)` → engine runs the request-animation-frame loop until `app.stop()` or `app.destroy()`. The render backend is chosen and initialized during `start()`; query Application.backend or Application.capabilities after start has resolved. The class exposes Signals for the major state-change points (Application.onResize, Application.onFrame, Application.onCanvasFocusChange, Application.onVisibilityChange, Application.onBackendLost, Application.onBackendRestored) so subscribers can react without subclassing. `pauseOnHidden = true` short-circuits the per-frame work while `document.hidden` is true (still consumes RAF callbacks but skips scene update + render). Useful for games; leave off for tools and background-active simulations. **Several applications on one page** are a supported shape, and each owns its surface, backend, scene stack, core systems, extension set, asset loader, frame loop, RNG and changed-record index. Two applications therefore neither rotate each other's retained-plan window nor make each other's scene mutations record anything. What they do share is the process: the Web Audio context (deliberately, since a browser admits only a few) and the monotonic revision counters the scene graph stamps nodes with, which advance faster with a second application but are only ever compared per node. A scene node belongs to exactly one application at a time, and moving one across is an ordinary reparent - its retained state travels with it.

38
props
10
methods
8
events
Import
import { Application } from '@codexo/exojs'

Top-level engine instance. Owns the canvas, render backend, scene-stack controller, the core systems (input, interaction, audio, coroutines, tweens, animations, rendering), the app-level SystemRegistry for user/extension systems, asset loader, and the per-frame loop.

Lifecycle: construct with options → `await app.start(scene)` → engine runs the request-animation-frame loop until `app.stop()` or `app.destroy()`. The render backend is chosen and initialized during `start()`; query Application.backend or Application.capabilities after start has resolved.

The class exposes Signals for the major state-change points (Application.onResize, Application.onFrame, Application.onCanvasFocusChange, Application.onVisibilityChange, Application.onBackendLost, Application.onBackendRestored) so subscribers can react without subclassing.

`pauseOnHidden = true` short-circuits the per-frame work while `document.hidden` is true (still consumes RAF callbacks but skips scene update + render). Useful for games; leave off for tools and background-active simulations.

**Several applications on one page** are a supported shape, and each owns its surface, backend, scene stack, core systems, extension set, asset loader, frame loop, RNG and changed-record index. Two applications therefore neither rotate each other's retained-plan window nor make each other's scene mutations record anything.

What they do share is the process: the Web Audio context (deliberately, since a browser admits only a few) and the monotonic revision counters the scene graph stamps nodes with, which advance faster with a second application but are only ever compared per node. A scene node belongs to exactly one application at a time, and moving one across is an ordinary reparent - its retained state travels with it.

Constructors1
new(appSettings: ApplicationOptions<Registry>): Application<Registry>
Methods10
destroy(): Promise<void>
Tear down every owned subsystem (loader, the core systems - input, interaction, audio, coroutines, tweens, animations, rendering - the app system registry, backend, scene director, all clocks, all signals) and release event listeners. The application instance is unusable after this call. The page is left as it was found: the active sizing policy is detached, so its observers go and the CSS box it wrote is cleared, and a canvas the engine created itself is removed from the document. A canvas supplied through canvas.element belongs to the caller and stays in place, as does every element around it - no sizing policy ever styles the page itself. Fires the RAF halt synchronously (so no further frame runs after this call returns) and returns a Promise that fulfils once the rest of teardown has run: scenes - including every retained and preloaded scope, and any scene's own async unload() - is fully disposed FIRST, before the Loader, rendering context, audio system, or backend are destroyed, so a scene's teardown code never touches an already-destroyed dependency. This intentionally does not route through the public Application.stop, which fire-and-forgets its own scene-clear - that would race against scenes._dispose()'s own active-scope teardown for ownership of the same scope. destroy() instead halts the frame loop directly and lets scenes._dispose() own scene teardown entirely. destroy() called right after a stop() is covered by the same guarantee, not an exception to it: the scene teardown stop() fired and did not await is published on the director, and scenes._dispose() waits for it - including a still-pending Scene.unload() - before any dependency is destroyed. Every extension goes down with the application: the disposers Extension.install returned run in reverse installation order, after scene teardown and before any subsystem they might still reach for is released. An extension's lifetime is exactly this application's - there is no uninstall short of it. The returned Promise **never rejects**: teardown failures go to Application.onError and the log, exactly as they did when this was a fire-and-forget chain, and the remaining stages still run. Awaiting it therefore means "teardown is over", not "teardown succeeded" - which is what a caller reusing the canvas or asserting on released resources needs. Scene teardown is bounded: if scenes._dispose() has not settled within the grace period the engine reports a timeout and releases everything else anyway, rather than leaving the whole application pinned by one scene whose unload() never resolves. A scene that wants to cooperate with this should watch Scene.lifecycleSignal, which is aborted when its teardown begins - including for the incoming scene of a navigation still inside load(), which is aborted and awaited rather than left to finish preparing against subsystems this call has released. Idempotent: every call after the first returns the same Promise as the first and starts no second teardown. Application.state is Destroying while the returned Promise is pending and Destroyed afterwards.
resize(width: number, height: number): this
Set a new base resolution and re-derive the canvas geometry from it. With no sizing policy this is the whole story: the logical view, the CSS box and the backing store all move to width x height (the last one times pixelRatio), and Application.onResize reports the new logical size. It is also the seam an externally sized host drives through under ManualCanvasSizing, where the CSS box stays the page's. Under a policy that tracks its surroundings the base resolution is a reference rather than a result: the policy is re-attached and immediately commits the geometry the host actually calls for, so the logical size that ends up dispatched need not be the one passed here.
Convert a logical/design-space pixel coordinate - the space of Pointer.x/Pointer.y and node positions, e.g. 0..app.width - to a world position using the active camera. At the default centered camera this is the identity; with a panned/zoomed/rotated camera it undoes the transform. Equivalent to app.rendering.view.screenToWorld(x, y).
Set the surface cursor. Strings are passed through to the platform verbatim (CSS values like 'pointer', 'crosshair', or url(...)). Image-based sources are rasterized to a data: URL via the shared scratch canvas and used as the cursor image.
start(): Promise<Application<Registry>>
Initialize the render backend, await capability detection, and start the per-frame loop without activating a scene. Use start(target, data?) to start directly into a registered scene. Idempotent - if the application is already running the call is a no-op. On error the state returns to Stopped and the error propagates. A stop() or destroy() made while startup is still loading wins over it: the run still settles, but the state that call wrote is the one that stands, so a resolved start() does not by itself mean the state is Running.
Initialize the render backend, await capability detection, activate target - a registered string key, or a constructor registered in ApplicationOptions.scenes - and start the per-frame loop. Idempotent - if the application is already running the call is a no-op. On error the state returns to Stopped and the error propagates. A stop() or destroy() made while startup is still loading wins over it: the run still settles, but the state that call wrote is the one that stands, so a resolved start() does not by itself mean the state is Running.
Initialize the render backend, await capability detection, and start the per-frame loop without activating a scene. Use start(target, data?) to start directly into a registered scene. Idempotent - if the application is already running the call is a no-op. On error the state returns to Stopped and the error propagates. A stop() or destroy() made while startup is still loading wins over it: the run still settles, but the state that call wrote is the one that stands, so a resolved start() does not by itself mean the state is Running.
stop(): this
Halt the per-frame loop, unload the active scene, and stop the active + frame clocks. Leaves backend, input, audio, etc. intact - call Application.destroy to release everything. Acts whenever the frame loop is actually live (_frameLoopActive), including mid-start() - not only while _state is Running. A stop is allowed to interrupt a navigation - that is the point of it. Everything scene-related is therefore delegated to the single SceneDirector._stopAndClearActiveScene operation, which invalidates the navigation generation, aborts an in-flight transition session if there is one, and then unloads the active scene unconditionally. Splitting that into "abort" and "clear" steps is what used to let the navigation lock win the race and leave the scene standing; stop() itself never fails with a ConcurrentSceneNavigationError. That does not make the interrupted navigation's own lock disappear. A navigation suspended in a Scene.load()/init() that never settles keeps stop()'s interruption from ever reaching its own catch, so it holds the director's navigation lock indefinitely - and the next Application.start or SceneDirector.change after such a stop rejects with ConcurrentSceneNavigationError for as long as that load() stays pending. The stop still unloads the scene; it just cannot cancel a promise the scene never resolves. Any scene-teardown failure the interruption did not cause - a scene's own unload()/destroy() throwing - still surfaces through Application.onError. Scene teardown is asynchronous and fire-and-forget here: stop() returns as soon as the loop is halted, so a scene with an async unload() may still be settling afterwards. A subsequent Application.destroy still waits for that teardown before releasing anything the scene depends on; use destroy() when teardown ordering matters.
update(timestamp: number): this
One iteration of the per-frame loop. Invoked by requestAnimationFrame. When the document is hidden and pauseOnHidden is true, the frame clock is reset and the body is skipped - preventing a large delta spike on the first visible frame after resume. Each normal frame runs, in order: 1. **Pre-frame** - app.systems pre-frame phase, then the active scene's own systems' pre-frame phase. The engine's input, interaction, audio, tween, animation and rendering systems are ordinary systems in this phase, pinned to the head of it by their SystemOrder Core* values, so this frame's input snapshot is current before anything simulates. An application system registered without an explicit order runs after all of them. 2. **Fixed steps** (zero or more) - app.systems fixed-update phase, scenes.fixedUpdate() + the scene's systems fixed-update phase, Application.onFixedFrame. 3. **Update** - app.systems update phase, then scenes.update() + the scene's systems update phase. 4. **Draw** - the canvas is cleared to Application.clearColor (unless autoClear: false), then the scene draws (plus its systems and UI layer); an active transition session's own visual output composites either below or above the app.systems draw phase depending on the session's placement ('scene': below app overlays; 'screen': above them, matching the pre-transition-runtime default). 5. **Frame dispatch / flush** - Application.onFrame, backend GPU flush, frame-time stat write. 6. **Post-frame** - app.systems post-frame phase, then the active scene's own systems' post-frame phase, both handed the frame's remaining time (FrameBudget). Work placed here overlaps the GPU drawing the frame just submitted. Running one frame is all this does: scheduling belongs to the loop, so a manual call runs an extra frame alongside a live loop rather than forking a second one, and does not restart a loop that Application.stop has halted - the body is skipped entirely while the loop is not live. The simulation delta forwarded to all update recipients is clamped to an internal maximum (100 ms) so that debugger pauses, device sleep/resume, or severe browser scheduling gaps cannot produce runaway animation advancement. Real wall-clock time and RAF cadence are unaffected; the raw elapsed delta is recorded separately in backend.stats.rawFrameDeltaMs.
Properties38
Drives frame playback for every AnimatedSprite that is playing and attached to this application's scene tree. Registration is automatic - see AnimationSystem.
The surface this application renders into - the canvas element it created or was given, or an OffscreenCanvas it was handed. width/height are the backing store in device pixels, not the CSS box; see Application.width for the design size. Use Application.element for anything that needs the surrounding document: styling, layout, or the element itself.
Whether the application may reach the network right now, and what the host reports about it. An ordinary runtime service, not a cache detail: UI reads it for an offline banner, and the asset cache reaches it only through a ConnectivityPolicyResolver the application was configured with.
Frame-budgeted driver for generator coroutines (world generation, batch pathfinding, anything too heavy for one frame). Runs in the postFrame phase on a share of what the frame has left; see CoroutineSystem.
element: HTMLCanvasElement | null
The render surface as a document canvas, or null when the application renders into an OffscreenCanvas and there is no element to reach.
loader: Loader
options: ApplicationOptions<Registry>
pauseOnHidden: boolean
The host seam this application runs on. Every part of the engine that has to reach outside its own state - input events, surface focus, cursor, pointer capture, gamepads, document visibility, frame scheduling - goes through this one adapter. See ApplicationOptions.platform.
App-scoped serializer registry, chained to the global defaultSerializationRegistry. Extension serializers materialise here rather than globally, so two Application instances in one process keep their extension serializers isolated; core and globally-registered (via registerSerializer) serializers remain shared through the fallback.
App-level system registry for user/extension systems - Application lifetime, independent of the active scene. The core systems (input, interaction, audio, coroutines, tweens, animations, rendering) are driven directly by the internal per-frame prepare stage and never occupy this registry, so any order is available; see SystemOrder for common reference points. Scene-scoped systems live on scenes.systems.
backend: RenderBackend
Low-level render backend. Prefer the high-level Application.rendering render context for normal rendering. Direct backend access is an escape hatch for custom render passes and advanced GPU work.
canvasFocused: boolean
Resolved capabilities for the host browser. Available after Application.start resolves; reading before that throws. For pre-start access use Capabilities.ready directly.
The colour the canvas is cleared to at the start of each frame, as a live Color. Assigning copies into the backend's clear colour (effective next frame); you may also mutate it in place via app.clearColor.set(...). The per-frame clear itself can be turned off with ApplicationOptions.autoClear, which leaves this the colour a manual context.clear(app.clearColor) uses.
cursor: string
documentVisible: boolean
fixedTimeStep: number
Fixed-timestep size in seconds (see ApplicationOptions.fixedTimeStep).
frameAlpha: number
Interpolation factor [0, 1) - the leftover sub-step fraction after this frame's fixed steps. Lerp rendered state between its previous and current fixed-step values by this to smooth motion when the fixed rate is below the frame rate.
frameCount: number
Passes that run after the frame has been drawn, with the frame itself as their input - the seam for a screen-wide effect (bloom over everything, a deferred lighting composite, a CRT filter) that a per-node filter cannot express. While this pipeline holds at least one pass, the scene, the systems' draw hooks and any scene transition render into Application.frameTexture instead of the canvas, and the pipeline is played afterwards; the last pass to write the active target produces the picture. An empty pipeline is the frame as it is drawn without this feature, at no cost. ts app.framePasses.addPass(new FilterPass(app.frameTexture, [new BloomFilter()])); Owned by the application: its passes are destroyed with it. Remove a pass before destroying an extension that owns it, the same contract every RenderPipeline has.
The off-screen target the frame is drawn into while framePasses holds passes - the source a frame pass reads. Sized to the logical surface times pixelRatio in texels, with a view in logical units, so the frame is rasterized at the density the canvas is and a pass sees the coordinates the scene was drawn in. It follows every resize, so a pass built once against it stays valid for the application's life. Reading this allocates it. An application that never adds a frame pass never pays for it.
height: number
Height of the logical coordinate system. See Application.width.
pixelRatio: number
Device pixels per CSS pixel the backing store is scaled by. Defaults to the host devicePixelRatio clamped to 2 - crisp on HiDPI out of the box, without the fill-rate cost a DPR-3 phone would otherwise pay - unless an explicit canvas.pixelRatio option was given. On that default it follows the host: moving the window to a display of a different density, or zooming the page, re-derives the backing store at the new ratio wherever the host can report the change. An explicit canvas.pixelRatio never tracks anything. It converts a requested render resolution into backing-store pixels and nothing else. Which render resolution is requested is the sizing policy's decision, so app.canvas.width is pixelRatio times that resolution, not times Application.width: the two coincide only while the logical view and the render resolution are the same size.
Bounded (20 entries) list of recent engine errors, newest last. Populated by the frame guard and by asynchronous backend render errors; feeds the debug dump. See Application.onError for live notification.
High-level rendering context. Routes scene drawing through the RenderPlan pipeline (build → optimize → play) and provides off-screen capture via RenderingContext.renderTo. Exposes the raw RenderBackend for advanced / custom-renderer use.
The active sizing policy, or null when the canvas simply stays at the base resolution. Assigning swaps the strategy live: the outgoing policy is detached - its observers released and the CSS box it wrote cleared - the canvas returns to the base geometry, and only then is the new policy attached, so no remnant of the previous one survives the switch. Assigning the policy that is already active still detaches and re-attaches it, which is the supported way to make one re-read a host it cannot observe by itself. The application does not take ownership: a detached policy is left intact and can be attached again later.
Where this application currently sits in its lifecycle. Same vocabulary as Scene.state - see ApplicationState for the table.
width: number
Width of the logical coordinate system the application draws in - the space of node positions and pointer coordinates. Use it for layout math (app.width / 2 to centre) rather than app.canvas.width, which is the backing store in device pixels. Equal to the base resolution canvas.width unless a sizing policy derives a different view from the host, which is what ResponsiveCanvasSizing does. It is not the CSS size of the canvas, and it is not the backing store divided by pixelRatio: all three are separate axes. Application.onResize reports every change.
Events8
Dispatched for every engine error: an exception thrown by any part of the per-frame body (systems tick, fixed steps, scene update/draw, Application.onFrame subscribers, backend flush - including synchronous WebGL2 shader compile/link failures, which surface as RenderErrors), an asynchronous GPU error reported by the backend (RenderBackend.onRenderError - WGSL compilation errors, WebGPU uncaptured validation/OOM/internal errors, and WebGPU device-recovery exhaustion), or a scene-unload failure in Application.stop. The frame guard keeps the loop alive through intermittent failures and halts it (state Stopped) after 3 consecutive failing frames. Narrow with error instanceof RenderError for structured GPU failure details; see Application.recentErrors for the bounded error history.
Fires whenever the canvas geometry changes, with the current logical width and height - the values Application.width/Application.height now report. Those two need not have moved: a policy that holds the logical view while the display size or the render resolution follows the host still dispatches, so a listener that caches something at the backing resolution has a signal to rebuild on. Read app.canvas.width/height for that resolution.
Source