Skip to content

Assets

import { boot } from "@f0rbit/forge/pixi";
const r = await boot({
mount: "#root",
assets: [
{ kind: "image", alias: "hero", url: "/sprites/hero.png" },
{ kind: "atlas", alias: "tilemap", url: "/atlases/tilemap.json" },
],
// ...
});

assets returns Result<App, BootError> — if any asset fails, boot disposes the renderer and returns err({ kind: "asset_failed", alias, cause: AssetError }). Boot loads assets sequentially in the order given.

The async loader is unified by a kind discriminator. The return type narrows on the kind — "image" yields Texture, "atlas" yields Spritesheet:

const a = app.assets;
const tex_r = await a.load("image", "hero", "/sprites/hero.png");
if (tex_r.ok) tex_r.value; // Texture
const sheet_r = await a.load("atlas", "hero_atlas", "/sprites/hero.json");
if (sheet_r.ok) sheet_r.value.textures["walk_0"]; // Spritesheet

Synchronous getters split by kind:

a.has("hero"); // → true
a.texture("hero"); // → Result<Texture, AssetError>
a.atlas("hero_atlas"); // → Result<Spritesheet, AssetError>
a.get<Spritesheet>("hero_atlas"); // → Result<T, AssetError> (untyped)
a.dispose(); // clear cache
type AssetKind = "image" | "atlas";
type LoadValue<K extends AssetKind> = K extends "image" ? Texture : Spritesheet;
type Assets = {
load: <K extends AssetKind>(kind: K, alias: string, url: string)
=> Promise<Result<LoadValue<K>, AssetError>>;
texture: (alias: string) => Result<Texture, AssetError>;
atlas: (alias: string) => Result<Spritesheet, AssetError>;
get: <T>(alias: string) => Result<T, AssetError>;
has: (alias: string) => boolean;
registry: () => AtlasRegistry;
register_atlas: (alias: string, sheet: Spritesheet) => Result<void, AssetError>;
dispose: () => void;
};
type AssetError =
| { kind: "load_failed"; alias: string; url: string; cause: string }
| { kind: "not_loaded"; alias: string }
| { kind: "invalid_atlas"; alias: string; issues: readonly string[] }
| { kind: "wrong_kind"; alias: string; expected: string };

Atlas JSON is the standard TexturePacker JSON-Hash format with optional animations arrays. Frame duration (ms) is converted to whole-tick durations using the configured fixed_dt — see Anim component.

For “I just want to see something on screen”, forge auto-registers a 4-frame placeholder atlas at boot:

AliasFramesPixels
__default____default_0__, __default_1__, __default_2__, __default_3__16×16 each, magenta / cyan / yellow / black

It also defines a spin sequence (all four frames at 100ms each) so anim_c.play("__default__", "spin") works out of the box.

w.set(player, sprite_c, {
texture: "__default__",
frame: "__default_0__",
anchor: { x: 0.5, y: 0.5 },
});

This is the pattern used in coin-collector/src/main.ts.

To skip the default atlas (e.g., production builds with no missing-art fallback):

const a = assets({ register_default: false });

(Pass your own assets instance via the boot API isn’t currently supported — register_default: true is hard-coded inside boot. Bring-your-own atlases simply override entries with the same alias if you ever load "__default__" yourself.)

The pair a.image(alias, url) / a.atlas(alias, url) async loaders are unified as a.load(kind, alias, url):

// v0.2.0
await a.image("hero", "/sprites/hero.png");
await a.atlas("hero_atlas", "/sprites/hero.json");
// v0.3.0
await a.load("image", "hero", "/sprites/hero.png");
await a.load("atlas", "hero_atlas", "/sprites/hero.json");

The synchronous getter a.texture(alias) is unchanged. A new synchronous getter a.atlas(alias) returns the previously-loaded Spritesheet (mirrors texture for atlases). The BootOpts.assets: AssetSpec[] consumer-side shape is unchanged.