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-physics

v0.19.1

Published

Static physics colliders from ExoJS tilemap collision geometry.

Readme

@codexo/exojs-tilemap-physics

Static physics colliders from ExoJS tilemap collision geometry.

A peer-dependency library on top of @codexo/exojs, @codexo/exojs-tilemap and @codexo/exojs-physics — construct its API directly. Importing it registers nothing.

@codexo/exojs-tilemap never imports physics and @codexo/exojs-physics never imports tilemap; this package is the seam between them, so both stay usable on their own.

Installation

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

Tile layers

TileColliderStreamer keeps a physics world's static tile colliders in sync with a TileLayer: one static body per chunk-sized partition of the layer, rebuilt when that partition is edited, destroyed when its chunk is evicted.

import { PhysicsWorld } from '@codexo/exojs-physics';
import { TileColliderStreamer } from '@codexo/exojs-tilemap-physics';

const world = new PhysicsWorld({ gravity: { x: 0, y: 1600 } });
const colliders = new TileColliderStreamer(world, groundLayer);

scene.systems.add({
  update: delta => {
    colliders.sync();
    world.step(delta);
  },
});

sync() is cheap to call every frame: with no change since the last call it returns immediately and does no work. It observes the layer through its public revision counters, so it works with ChunkStreamer, a hand-rolled loader, or a bounded layer that is fully resident.

Call destroy() to remove every body the bridge created. Bodies it did not create are untouched.

Collision authored per cell

Some editors author collision per grid cell rather than per tile - LDtk's IntGrid is the common case. Pass a cells source and the bridge covers the whole bounded layer, including partitions that hold no tiles at all:

import { createLdtkIntGridCellSource } from '@codexo/exojs-ldtk';

const colliders = new TileColliderStreamer(world, collisionLayer, {
  cells: createLdtkIntGridCellSource(collisionLayer),
  material: ({ type }) => (type === 'Water' ? { isSensor: true } : null),
});

A cell source is a plain (tx, ty) => string | null, so a procedural or hand-rolled grid works the same way. The returned string is a classification, never a meaning: it is the merge key for adjacent cells and it is what the material resolver sees. Nothing in this package knows what Solid or Water are supposed to do.

The source is sampled while a partition is built and is expected to answer identically for the lifetime of the bridge. Changing what it returns does not invalidate colliders that already exist - rebuild by recreating the bridge.

Object layers

An object layer is static data with no residency, so it gets a one-shot build:

import { buildObjectLayerColliders } from '@codexo/exojs-tilemap-physics';

const built = buildObjectLayerColliders(world, triggersLayer, {
  isSensor: true,
});

for (const { object, body } of built) {
  // `object` is the source object: name, type, custom properties.
}

Isometric presentation

Tile and object colliders use logical coordinates. Isometric logical cells are squares of side tileHeight; image-space tile collision shapes are inverse-projected before collider generation. Display layer offsets become logical translation vectors, and streamed collider bodies retain the same cell/chunk ownership rules. Orthogonal behavior is unchanged.

Create a TilePhysicsBinding(body, node, map.projection) explicitly and call sync() after physics updates, or syncInterpolated(alpha) with the fixed-step interpolation fraction. The node's parent must use map display coordinates. The binding only writes node position, owns nothing, and leaves sprite rotation/scale to the application; a projected logical rotation is not a rigid sprite rotation. Do not attach the same node through PhysicsWorld.attach(), which binds directly to unprojected world coordinates.

Object-layer objects remain logical geometry; getDisplayObject() is for rendering and must not be fed back into the collider builder. The Isometric Landscape example shows both representations.

Geometry mapping

| Source | Collider | | -------------------------------------------------- | ---------------------------------------------------- | | Merged whole-cell region (regionMode: 'boxes') | one box per merged rectangle | | Merged whole-cell region (regionMode: 'outline') | one closed chain per boundary loop | | Rectangle | box | | Ellipse | capsule along the major axis, or a circle when round | | Convex polygon | one polygon | | Concave polygon | several convex polygons on the same body | | Polyline | open chain; closed chain when its endpoints coincide | | Point | nothing |

An ellipse maps to the capsule with the minor semi-axis as its radius and the difference of the semi-axes as its spine. That capsule contains the ellipse and is contained in the circumscribed circle, so it covers the source without a tuning constant, and degenerates to a circle when the ellipse is round.

Region modes

regionMode: 'boxes' (the default) keeps merged rectangles as solid boxes. The region has an interior, so point queries, overlaps and sensors behave, and a body that starts inside solid tiles is pushed out. Where the merge cannot merge — neighbouring cells with different collision semantics, staircase profiles, partial cells — adjacent boxes share an internal edge that a body sliding across can catch on.

regionMode: 'outline' traces the boundary of each solid region into closed one-sided chains, which have no internal edges at all. It removes intra-chunk seams between adjacent cells that resolve to the same collision semantics; boundaries between regions with different resolved semantics remain separate, as do chunk boundaries. The trade-off is that a chain is a boundary, not an area: queries inside the region find nothing, and a body spawned inside it falls through.

Materials

Friction, restitution, density, the sensor flag and the collision filter are call-level defaults, optionally overridden per object:

new TileColliderStreamer(world, groundLayer, {
  friction: 0.8,
  material: ({ type }) => (type === 'ice' ? { friction: 0.02 } : null),
});

The resolver is a build-time mapping, not a live rule: it runs only while a chunk is being built or rebuilt. Changing what it would return has no effect until the layer itself changes. An unrecognised type is not an error — it reaches the resolver like any other and falls back to the defaults.

In outline mode the resolved material also decides what may share a boundary: cells whose colliders would be indistinguishable are traced into one chain, and cells that resolve differently keep their own.

Things to know before shipping a level

  • Merging never crosses a chunk boundary. A solid run spanning two chunks is at least two rectangles. That is what makes the result independent of the order chunks were loaded in.
  • A dynamic body resting on an evicted chunk falls. Eviction is the chunk source's decision; widen its unload radius if that matters.
  • Order is stable, part counts are not. For a given layer and resident chunk set the colliders are identical regardless of load order, and bodies() iterates in (cy, cx) chunk order. The number of convex parts a concave polygon decomposes into, and the exact rectangle decomposition of a merged region, are not contractual.

Core compatibility

This package follows the Core lockstep release line. Its Core, tilemap, and physics peer dependencies require the matching minor release.

License

MIT © Codexo