Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions site/src/content/guide/assets/aseprite.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
---
title: 'Aseprite sprite sheets'
description: 'Load Aseprite JSON sprite sheet exports as AsepriteSheet assets through the official @codexo/exojs-aseprite extension.'
---

# Aseprite sprite sheets

[Aseprite](https://www.aseprite.org/) is a popular pixel-art and animation editor. The official
`@codexo/exojs-aseprite` extension adds an `asepriteSheet` asset type that loads Aseprite's JSON
sheet export through the normal [`Loader`](/ExoJS/en/api/loader/) pipeline — fetching the JSON,
resolving and loading the packed image as a `Texture`, and returning an
[`AsepriteSheet`](/ExoJS/en/api/aseprite-sheet/) with a frame `Spritesheet` and one playable clip
per Aseprite tag.

> **Note:** `AsepriteSheet` ships as an official ExoJS extension package, separate from the core.
> Install `@codexo/exojs-aseprite` alongside `@codexo/exojs`:
> ```sh
> npm install @codexo/exojs @codexo/exojs-aseprite
> ```

Export your sprite from Aseprite as a JSON sheet (**File → Export Sprite Sheet**, with *Output → JSON
Data* enabled). Either *Array* or *Hash* frame layout works, and tags become animation clips.

Like all extensions, the core ships nothing Aseprite-specific — an `Application` only understands
the `asepriteSheet` type once you activate the extension. There are two ways to do that.

## Activation

### Explicit (recommended)

Pass `asepriteExtension` to `ApplicationOptions.extensions`. This is explicit, tree-shakeable, and
order-independent — the extension is bound to exactly this `Application`:

```js
import { Application } from '@codexo/exojs';
import { asepriteExtension } from '@codexo/exojs-aseprite';

const app = new Application({ extensions: [asepriteExtension] });
```

The package root (`@codexo/exojs-aseprite`) is side-effect-free: importing it registers nothing
globally. You decide which `Application` gets the extension.

### `/register` convenience

For an app that always wants Aseprite support, import the `/register` entry once at startup. It
registers `asepriteExtension` in the global `ExtensionRegistry` and re-exports the same public API:

```js
import '@codexo/exojs-aseprite/register';
import { Application } from '@codexo/exojs';

const app = new Application(); // picks up asepriteExtension automatically
```

The `/register` import must run **before** you construct the `Application` whose extensions are
read from the global registry. The explicit `extensions: [...]` form works regardless of import
order.

### Core-only

An `Application` constructed with an explicit empty list takes **no** extensions — not even
globally registered ones — and will not recognise the `asepriteSheet` type:

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

const app = new Application({ extensions: [] }); // core only; no AsepriteSheet
```

This is the same add-only model the [Tiled maps](/ExoJS/en/guide/assets/tiled-maps/) and
[Particles](/ExoJS/en/guide/effects/particles/) extensions use.

## Loading a sheet

Once the extension is active, load the JSON export by the `AsepriteSheet` type token or by the
`'asepriteSheet'` config-map type name:

```ts no-check
import { AsepriteSheet } from '@codexo/exojs-aseprite';

// By type token (most explicit)
const sheet = await loader.load(AsepriteSheet, 'sprites/hero.json');

// By config-map type name — the public `'asepriteSheet'` lookup
const fromConfig = await loader.load({
hero: { type: 'asepriteSheet', source: 'sprites/hero.json' },
});
```

Unlike the Tiled adapter, the Aseprite binding does **not** claim a file extension — a bare
`loader.load('sprites/hero.json')` will not route here. Always name the `AsepriteSheet` token or the
`'asepriteSheet'` type. The handler fetches the JSON, validates it (throwing `AsepriteFormatError`
on a malformed document), resolves `meta.image` relative to the JSON source, sub-loads it as a
`Texture` through the same loader, and returns a fully-parsed `AsepriteSheet`.

## Playing tag animations

`AsepriteSheet.createAnimatedSprite()` returns an
[`AnimatedSprite`](/ExoJS/en/api/animated-sprite/) with every Aseprite tag pre-defined as a named,
playable clip. Call `play(tag)` with a tag name to start it:

```js
async load(loader) {
this.sheet = await loader.load(AsepriteSheet, 'sprites/hero.json');
}

init() {
this.player = this.sheet.createAnimatedSprite();
this.player.play('walk');
this.root.addChild(this.player);
}

update(delta) {
this.player.update(delta); // advance the active clip
}
```

Like any `AnimatedSprite`, you must call `player.update(delta)` each frame to advance playback — see
the [Animation](/ExoJS/en/guide/rendering/animation/) guide for the full clip/playback API.

The parsed clips are also exposed directly on `sheet.clips` — a read-only `Map` keyed by tag name —
so you can inspect what was imported or build a sprite by hand:

```js no-check
sheet.clips.size; // number of tags
sheet.clips.has('walk'); // true when the 'walk' tag exists
sheet.clips.keys(); // iterate tag names

sheet.spritesheet; // the underlying Spritesheet (frames keyed by index string)
```

## Frame timing (fps)

Each clip's `fps` is derived from the per-frame `duration` values Aseprite exports (milliseconds per
frame), averaged across the tag's `from`→`to` range:

- A tag whose frames are all 100 ms produces **10 fps**; mixed durations are averaged (100/200/300 ms
→ 200 ms → **5 fps**).
- When every frame duration in the tag is zero, the clip falls back to **12 fps**.

Every clip is created with `loop: true`. Aseprite's ping-pong directions are recorded as looping but
are **not** expanded into a reversed segment — a ping-pong tag plays only its declared `from`→`to`
range. Frame indices in a tag that fall outside the frame array are skipped, and a tag whose entire
range is out of bounds produces no clip.

## Not yet supported

- **Slices** — slice data (`AsepriteSlice` / `AsepriteSliceKey`) is parsed into the raw document
types but is **not** yet surfaced as a runtime API; there is no slice accessor on `AsepriteSheet`.
- **Layers / nested tags** — Aseprite layer metadata is preserved in the parsed data but is not
interpreted into clips.

## Texture ownership

The packed image an `AsepriteSheet` uses is loaded through the `Loader` and owned by the **loader
cache**. `AsepriteSheet.destroy()` releases the underlying `Spritesheet` frames; release the shared
texture through the loader's own unload path when nothing else needs it.

## Where to look next

- **Package README:** [`@codexo/exojs-aseprite`](https://github.com/Exoridus/ExoJS/blob/main/packages/exojs-aseprite/README.md)
— install, entry points, and the supported-format matrix.
- **API reference:** [`AsepriteSheet`](/ExoJS/en/api/aseprite-sheet/) documents `clips`,
`spritesheet`, and `createAnimatedSprite()`; [`AnimatedSprite`](/ExoJS/en/api/animated-sprite/)
documents clip definition and playback; [`Loader`](/ExoJS/en/api/loader/) documents `load(...)`
and the `'asepriteSheet'` config-map type name.
- **Guides:** [Sprites](/ExoJS/en/guide/rendering/sprites/) and
[Loading & resources](/ExoJS/en/guide/assets/loading-and-resources/) cover the rendering and asset
model the sheet plugs into.
169 changes: 169 additions & 0 deletions site/src/content/guide/assets/ldtk.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
---
title: 'LDtk levels'
description: 'Load LDtk (.ldtk) level files and convert each level to a renderable TileMap through the official @codexo/exojs-ldtk extension.'
---

# LDtk levels

[LDtk](https://ldtk.io/) is a modern, open-source level editor. The official `@codexo/exojs-ldtk`
extension adds an `ldtkMap` asset type that loads an LDtk project (`.ldtk`) through the normal
[`Loader`](/ExoJS/en/api/loader/) pipeline — fetching the JSON, loading every referenced tileset
image, and converting **each LDtk level** into a runtime [`TileMap`](/ExoJS/en/api/tile-map/) you
can drop straight into the scene graph.

> **Note:** `LdtkMap` ships as an official ExoJS extension package, separate from the core. It is
> built on `@codexo/exojs-tilemap` (a regular dependency, installed transitively — you do not need
> to add it yourself). Install `@codexo/exojs-ldtk` alongside `@codexo/exojs`:
> ```sh
> npm install @codexo/exojs @codexo/exojs-ldtk
> ```

Because each LDtk level becomes a generic `TileMap`, you render LDtk worlds with the same tilemap
node used everywhere else in the engine — there is no LDtk-specific renderer.

Like all extensions, the core ships nothing LDtk-specific — an `Application` only understands
`.ldtk` files once you activate the extension. There are two ways to do that.

## Activation

### Explicit (recommended)

Pass `ldtkExtension` to `ApplicationOptions.extensions`. This is explicit, tree-shakeable, and
order-independent — the extension is bound to exactly this `Application`:

```js
import { Application } from '@codexo/exojs';
import { ldtkExtension } from '@codexo/exojs-ldtk';

const app = new Application({ extensions: [ldtkExtension] });
```

`ldtkExtension` depends on `tilemapExtension`, so activating it enables **both** loading and tilemap
rendering — the tile-chunk renderer bindings are materialised automatically. The package root
(`@codexo/exojs-ldtk`) is side-effect-free: importing it registers nothing globally.

### `/register` convenience

For an app that always wants LDtk support, import the `/register` entry once at startup. It
registers `ldtkExtension` (and its `tilemapExtension` dependency) in the global `ExtensionRegistry`
and re-exports the same public API:

```js
import '@codexo/exojs-ldtk/register';
import { Application } from '@codexo/exojs';

const app = new Application(); // picks up ldtkExtension automatically
```

The `/register` import must run **before** you construct the `Application` whose extensions are read
from the global registry. The explicit `extensions: [...]` form works regardless of import order.

### Core-only

An `Application` constructed with an explicit empty list takes **no** extensions — not even
globally registered ones — and will not recognise `.ldtk` files:

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

const app = new Application({ extensions: [] }); // core only; no LdtkMap
```

This is the same add-only model the [Tiled maps](/ExoJS/en/guide/assets/tiled-maps/) extension uses.

## Loading a world

Once the extension is active, load a `.ldtk` file by type token, by path (the `.ldtk` extension is
registered), or by the `'ldtkMap'` config-map type name:

```ts no-check
import { LdtkMap } from '@codexo/exojs-ldtk';

// By type token (most explicit)
const world = await loader.load(LdtkMap, 'https://example.com/levels/world.ldtk');

// By path — the `.ldtk` extension is registered, so the type is inferred
const sameWorld = await loader.load('https://example.com/levels/world.ldtk');

// By config-map type name — the public `'ldtkMap'` lookup
const fromConfig = await loader.load({
world: { type: 'ldtkMap', source: 'https://example.com/levels/world.ldtk' },
});
```

> **Note:** the LDtk loader resolves tileset image paths against the map's own URL with the `URL`
> constructor, which requires that URL to be **absolute (origin-qualified)**. Load `.ldtk` files by
> an absolute `https://…` URL, or set `loader.basePath` to an absolute origin so relative map paths
> resolve to absolute URLs. A bare relative or root-relative path will fail tileset resolution.

## The LdtkMap result

A loaded `LdtkMap` exposes one runtime `TileMap` per LDtk level, plus the raw parsed document:

```js no-check
world.levels; // readonly TileMap[] — one per level, in document order
world.getLevelByName('Level_0'); // TileMap | undefined — lookup by LDtk identifier
world.data; // the raw parsed LdtkData document
```

Each level's `TileMap` carries the LDtk tile layers (as `TileLayer`s) and entity layers (as data-only
`ObjectLayer`s, with each entity's position, size, and scalar fields preserved as object
properties). World-space placement is stored in `TileMap.properties` (`worldX` / `worldY`).

## Rendering a level

Wrap a level's `TileMap` in a `TileMapNode` and add it to the scene. `TileMapNode` is the generic
tilemap node from `@codexo/exojs-tilemap` (the package LDtk builds on):

```ts no-check
import { TileMapNode } from '@codexo/exojs-tilemap';

const level = world.getLevelByName('Level_0') ?? world.levels[0];
scene.root.addChild(new TileMapNode(level));
```

To stream a multi-level world, add a node per level — each level is its own `TileMap`:

```js no-check
for (const level of world.levels) {
scene.root.addChild(new TileMapNode(level));
}
```

`TileMapNode` handles per-chunk culling and renders the level's layers in document order. For
interleaving actors between tile layers, use `TileMap.createView()` to obtain independently
placeable layer nodes — see the [`TileMapView`](/ExoJS/en/api/tile-map-view/) API.

## Converting manually (advanced)

The loader does the fetch-and-convert for you, but the conversion step is also available directly.
`ldtkToTileMap` turns a raw `LdtkData` document into an `LdtkMap` of runtime `TileMap`s — useful for
custom pipelines or for converting data you already hold in memory:

```ts no-check
import { ldtkToTileMap } from '@codexo/exojs-ldtk';

const converted = ldtkToTileMap(world.data, { source: 'https://example.com/levels/world.ldtk' });
```

Without an `options.tilesets` map, tile layers are created with correct dimensions but no tile data —
the asset-loading path supplies the runtime tilesets for you. Tile flip flags are exposed as the
constants `ldtkFlipNone`, `ldtkFlipX`, `ldtkFlipY`, and `ldtkFlipXy` for reading raw `LdtkTileData`.

## Texture ownership

Tileset textures are loaded through the `Loader` and owned by the **loader cache** — not by the
`LdtkMap`. `LdtkMap.destroy()` destroys the owned runtime `TileMap`s but deliberately does **not**
unload tileset textures (they may be shared) or remove any scene nodes — the application owns those.

## Where to look next

- **Package README:** [`@codexo/exojs-ldtk`](https://github.com/Exoridus/ExoJS/blob/main/packages/exojs-ldtk/README.md)
— install, entry points, and the supported-feature matrix.
- **API reference:** [`LdtkMap`](/ExoJS/en/api/ldtk-map/) documents `levels`, `getLevelByName`, and
`data`; [`TileMap`](/ExoJS/en/api/tile-map/) and [`TileMapNode`](/ExoJS/en/api/tile-map-node/)
document the runtime tilemap and its renderer.
- **Sibling extension:** the [Tiled maps](/ExoJS/en/guide/assets/tiled-maps/) chapter covers the
same explicit-vs-`/register` activation for the other map-editor adapter.
- **Loading model:** [Loading & resources](/ExoJS/en/guide/assets/loading-and-resources/) covers the
asset pipeline the loader plugs into.
Loading