npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@codexo/exojs-tilemap

v0.19.1

Published

Generic, format-independent tilemap runtime for ExoJS.

Readme

@codexo/exojs-tilemap

Generic, format-independent tilemap runtime and WebGL2/WebGPU chunk renderer for ExoJS. No Tiled (or any other on-disk format) vocabulary leaks into this package - adapters such as @codexo/exojs-tiled parse their format and hand this runtime fully resolved tiles.

Installation

npm install @codexo/exojs @codexo/exojs-tilemap

@codexo/exojs is a peer dependency, so install it explicitly. This package is itself a peer of the format adapters (@codexo/exojs-tiled, @codexo/exojs-ldtk) rather than something they pull in transitively — install it whether you use an adapter or build procedural / custom-format maps directly.

What this package provides

Runtime (data model) — pure data, no scene graph:

  • TileMap — a finite, chunk-first map: dimensions, tile size, tilesets, ordered layers.
  • TileLayer — one tile layer; packed Uint32Array chunk storage, visible / opacity / offsetX/Y, queries, and revision tracking.
  • TileSet — a resolved tileset grid over a Core TextureRegion (Loader-owned texture).
  • ResolvedTile / TileTransform — value-typed tile references with flipX / flipY / diagonal orientation.

Worlds and level lifetime — format-neutral, driven by whichever adapter loaded the data:

  • MapWorld / MapLevel — where levels sit: stable id, world bounds, neighbour graph, externality. Metadata only; no tiles, no textures.
  • MapWorldRuntime — explicit loadLevel / unloadLevel, one child LoaderScope per level, at most one live level per id. No camera policy: the game decides what to load.
  • MapLevelRuntime — one loaded level's map, scope and spawn session, with a single destroy() that tears all three down in a safe order.

Map object spawning:

  • MapObjectSpawner — a local (never global) dispatch table from object class to a factory. Deterministic order, atomic rollback, AbortSignal cancellation.
  • MapObjectDescriptor — the format-neutral view of one authored object handed to a factory.
  • MapSpawnSession — owns what one spawn produced; constant-time lookup by stable source id, reverse-order teardown.

Scene / rendering (@advanced):

  • TileMapNode — convenience root that renders a whole TileMap.
  • TileLayerNode — renders one TileLayer; generated per layer by TileMapView (or construct directly).
  • TileMapView — groups a map's layers into independently placeable layer nodes and named TileMapBands for actor interleaving (a helper, not a scene node).
  • TileMapBand — a Container of tile-layer nodes produced by TileMapView.
  • tilemapExtension — extension descriptor carrying the WebGL2/WebGPU renderer bindings.

Usage — procedural map

import { Application, Scene, type Texture, TextureRegion } from '@codexo/exojs';
import { TileLayer, TileMap, TileMapNode, TileSet, tilemapExtension, TILE_TRANSFORM_IDENTITY } from '@codexo/exojs-tilemap';

class WorldScene extends Scene {
  private atlas: Texture | null = null;

  public override async load(): Promise<void> {
    // A Loader-owned atlas texture: the tilemap runtime never destroys it.
    this.atlas = await this.loader.load('tiles.png');
  }

  public override init(): void {
    const atlas = this.atlas!;
    const terrain = new TileSet({
      name: 'terrain',
      texture: new TextureRegion(atlas, { x: 0, y: 0, width: atlas.width, height: atlas.height }),
      tileWidth: 16,
      tileHeight: 16,
      tileCount: 256,
    });

    const ground = new TileLayer({
      id: 1,
      name: 'ground',
      width: 64,
      height: 64,
      tileWidth: 16,
      tileHeight: 16,
      tilesets: [terrain],
    });
    ground.setTileAt(0, 0, { tileset: terrain, localTileId: 5, transform: TILE_TRANSFORM_IDENTITY });

    const map = new TileMap({
      name: 'world',
      width: 64,
      height: 64,
      tileWidth: 16,
      tileHeight: 16,
      tilesets: [terrain],
      layers: [ground],
    });

    // Render the whole map: a scene draws its root without a draw() override.
    this.addChild(new TileMapNode(map));
  }
}

const app = new Application({ extensions: [tilemapExtension] /* canvas, … */ });

await app.start(WorldScene);

Interleaving actors between layers — TileMapView

TileMapNode owns only the map's layer nodes and renders them back-to-front — use it when nothing renders between layers. To draw application actors between tile layers, create a TileMapView: it generates one TileLayerNode per map layer (stable identity, map document order) and groups them into named TileMapBands that you parent yourself, as siblings of your own actor containers:

const view = map.createView({
  bands: {
    ground: ['background', 'ground'],
    roof: ['roofs', 'foreground'],
  },
});

worldRoot.addChild(
  view.band('ground'),
  actors, // app-owned actor container — drawn between ground and roof
  view.band('roof'),
);

Or, without bands, place the generated per-layer nodes directly:

const view = map.createView();

worldRoot.addChild(view.getLayerNodeById(groundId)!, actors, view.getLayerNodeById(roofId)!);

Actors are application-owned siblings. TileMapView never adopts or destroys actors.

  • A band definition selects layers (by id, or by unique layer name); rendering order within a band always follows map document order — definitions never reorder layers.
  • Layers not listed in any band stay reachable via view.getLayerNodeById(id) / view.getLayerNodesByName(name); they are not auto-added to any band.
  • Destroying a view or a band destroys only the tile nodes it generated — never actors, the TileMap, its TileLayers, or tileset textures.
  • There is no map-replacement API: to swap maps, destroy the old view and create a new view from the new map — the actor tree is untouched.
  • After layers are structurally added to or removed from the map, call view.refreshLayers(): unchanged layer nodes keep their identity and bands keep their placement in your scene graph.

Isometric grids

Supply a shared TileProjection({ orientation: 'isometric', tileWidth, tileHeight, originX, originY }) to the map and each tile layer. Orthogonal is the default. tileToPixel() projects the cell's top diamond vertex; pixelToTile() inversely projects and floors both grid axes, including negative coordinates. Cell boundaries are half-open. projection.logicalToPixel() and pixelToLogical() preserve fractional coordinates. Isometric logical cells are squares of side tileHeight; display diamonds may have any positive integer width and height.

Layer offsets are display translations. TileLayer.logicalOffset exposes the corresponding logical vector for simulation. A map's pixel dimensions describe the projected grid AABB, while projection.getBounds() also supplies its possibly negative origin. Scene-node bounds include isometric artwork overhangs and tileset offsets. Picking describes cells, not opaque pixels of tall artwork; apply inverse scene and camera transforms before calling the map/layer helpers.

Isometric rendering splits resident storage chunks into diagonal draw sections, sorted across chunks by increasing tx + ty, then increasing tx. Only consecutive tiles with the same texture are batched, so tall tiles preserve painter order across texture and chunk boundaries. Geometry stays revision-cached. All sections share a layer-grid snap origin, including odd tile sizes. More draw sections and texture changes can cost more than orthogonal batching; no throughput improvement is promised.

Object layers remain data-only. With a projection their objects use logical coordinates; layer.getDisplayObject(object) returns display geometry with the layer offset applied. Rectangles may become polygons and ellipses retain their exact affine shape. Tile/text dimensions stay in display pixels. Actors remain application-owned; bands interleave them between layers, not automatically among individual terrain tiles.

Renderer model

  • Chunk-first. A TileLayerNode is a container of per-chunk TileChunkNode drawables (one per non-empty loaded chunk). The renderer batches tiles by (shader, tileset texture) and issues one instanced draw per batch — draw calls scale with visible chunks × layers, not total tile count.
  • Revision-cached geometry. Each chunk's quad geometry is built once and cached against the source chunk's revision. Unchanged chunks never rebuild; a camera pan rebuilds nothing — off-screen chunks are culled by their local bounds before any geometry is touched.
  • Orientation. flipX / flipY / diagonal are baked into the chunk geometry / resolved in the shader (all 8 combinations), with no per-tile matrix or per-frame cost.
  • Multiple tilesets with differing tile sizes are first-class; tiles taller than the map grid are bottom-left aligned (Tiled orthogonal convention).
  • WebGL2 and WebGPU share one CPU geometry builder and produce identical output (golden parity tested on both backends).
  • Sampling. Tile UVs are exact (no half-texel inset), which assumes nearest atlas filtering - the typical pixel-art case. Under linear or mipmap filtering, author tilesets with extruded tile margins to avoid neighbour bleed at tile edges; the tilemap renderer does not apply an extrusion-aware inset.

Ownership & lifecycle

  • Tileset textures are Loader-owned. The runtime and renderer never destroy them.
  • A MapWorldRuntime owns the scope it creates under the one you give it, plus one child scope per loaded level - never the scope you passed in. A MapLevelRuntime owns the TileMap its provider returned, its spawn session, and its scope; destroy() releases them in that order.
  • TileMapNode / TileMapView / TileMapBand / TileLayerNode reference — but never own — the TileMap. Destroying a map node, view, or band frees only the tile nodes it generated (detaching them from their application parents) and their cached geometry; application actors, the TileMap data, its TileLayers, and textures all survive. Free those via TileMap.destroy() and Loader.destroy() respectively.
  • A TileMapNode or TileMapView reflects the layer set at construction time. After layers are structurally added to or removed from the map call node.refreshLayers() / view.refreshLayers(); after tiles are written into previously-empty chunks call layerNode.refresh(). In-place edits to existing chunks are picked up automatically.

Core compatibility

This package follows the Core lockstep release line and declares the compatible @codexo/exojs minor as a peer dependency. Install matching package versions.

Links

License

MIT