Fully typed, tree-shakeable 2D physics engine — a modern TypeScript rewrite of the Nape Haxe physics engine.
Homepage & Interactive Demos | API Reference | Examples | Multiplayer Demo
Cookbook | Troubleshooting | Anti-Patterns
This repo is an npm-workspaces monorepo:
| Package | What it is |
|---|---|
@newkrok/nape-js |
The physics engine itself — rigid bodies, constraints, collision, fluids, deterministic multiplayer. |
@newkrok/nape-pixi |
PixiJS v8 integration — BodySpriteBinding, FixedStepper (render interpolation), PixiDebugDraw, WorkerBridge. |
The templates/platformer/ directory holds a
runnable multi-renderer starter (Canvas2D / Three.js / PixiJS) — clone
or download it as a starting point for new projects.
npm install @newkrok/nape-js
# optional: PixiJS v8 integration
npm install @newkrok/nape-pixi pixi.js
import { Space, Body, BodyType, Vec2, Circle, Polygon } from "@newkrok/nape-js";
// Create a physics world with downward gravity
const space = new Space(new Vec2(0, 600));
// Static floor
const floor = new Body(BodyType.STATIC, new Vec2(400, 550));
floor.shapes.add(new Polygon(Polygon.box(800, 20)));
floor.space = space;
// Dynamic box
const box = new Body(BodyType.DYNAMIC, new Vec2(400, 100));
box.shapes.add(new Polygon(Polygon.box(40, 40)));
box.space = space;
// Dynamic circle
const ball = new Body(BodyType.DYNAMIC, new Vec2(420, 50));
ball.shapes.add(new Circle(20));
ball.space = space;
// Game loop
function update() {
space.step(1 / 60);
for (const body of space.bodies) {
console.log(`x=${body.position.x.toFixed(1)} y=${body.position.y.toFixed(1)}`);
}
}
Full API documentation: TypeDoc Reference
| Class | Description |
|---|---|
Space |
Physics world — add bodies, step simulation, deterministic mode for rollback/prediction |
Body |
Rigid body with position, velocity, mass |
Vec2 |
2D vector — pooling, clone(), equals(), lerp(), fromAngle() |
Vec3 |
3D vector for constraint impulses — clone(), equals() |
AABB |
Axis-aligned bounding box — clone(), equals(), fromPoints() |
Mat23 |
2×3 affine matrix — clone(), equals(), transform, inverse |
Ray |
Raycasting — clone(), fromSegment(), spatial queries |
| Class | Description |
|---|---|
Circle |
Circular shape |
Polygon |
Convex polygon (with Polygon.box(), Polygon.rect(), Polygon.regular()) |
Capsule |
Capsule shape (Capsule.create(), Capsule.createVertical()) |
Shape |
Base class with material, filter, sensor support |
| Class | Description |
|---|---|
Material |
Elasticity, friction, density |
BodyType |
STATIC, DYNAMIC, KINEMATIC |
InteractionFilter |
Bit-mask collision/sensor/fluid filtering |
FluidProperties |
Density, viscosity for fluid shapes |
| Class | Description |
|---|---|
PivotJoint |
Pin two bodies at a shared point |
DistanceJoint |
Constrain distance between anchors |
WeldJoint |
Fix relative position and angle |
AngleJoint |
Constrain relative angle |
MotorJoint |
Apply angular velocity |
LineJoint |
Slide along a line |
PulleyJoint |
Constrain combined distances |
| Class | Description |
|---|---|
InteractionListener |
Collision/sensor/fluid events |
BodyListener |
Body wake/sleep events |
ConstraintListener |
Constraint events |
PreListener |
Pre-collision filtering |
CbType |
Tag interactors for filtering |
CbEvent |
BEGIN, ONGOING, END, WAKE, SLEEP, BREAK |
| Class | Description |
|---|---|
NapeList<T> |
Iterable list with for...of support |
MatMN |
Variable-sized M×N matrix — clone(), equals(), multiply, transpose |
Higher-level building blocks layered on top of the engine — opt-in modules.
| Helper | Description |
|---|---|
CharacterController |
Velocity-based 2D platformer controller — ground / slope / wall raycasts, coyote-time, one-way platforms, moving-platform inheritance, runtime-mutable down for radial-gravity worlds |
RadialGravityField / RadialGravityFieldGroup |
Point-source gravity well — inverse-square / inverse / constant / custom falloff, maxRadius / softening, body filter, mass scaling. Replaces hand-rolled body.force = ... loops |
ParticleEmitter / ParticleEmitterGroup |
Physics-aware particle emitter — pooled bodies, continuous / periodic / manual spawning, configurable spawn / velocity patterns, deterministic RNG, lifecycle hooks (onSpawn / onUpdate / onDeath / onCollide), self-excluding filter generation |
buildTilemapBody / meshTilemap |
Greedy-meshed collision body from a 2D tile grid. 5–50× fewer shapes than one-polygon-per-cell. Includes tiledLayerToGrid and ldtkLayerToGrid parsers |
TriggerZone |
Sensor zone with onEnter / onStay / onExit callbacks — wraps the BEGIN/ONGOING/END InteractionListener plumbing |
fractureBody |
Voronoi-based polygon shatter — fragmentCount, explosionImpulse, deterministic via random |
createConcaveBody |
Decompose a concave outline into convex polygons and pack them into a single body |
Full physics state snapshot/restore — suitable for save/load, replay, and multiplayer server↔client synchronization.
import "@newkrok/nape-js";
import { spaceToJSON, spaceFromJSON } from "@newkrok/nape-js/serialization";
// Serialize
const snapshot = spaceToJSON(space);
const json = JSON.stringify(snapshot);
// Restore (e.g. on another machine / after network transfer)
const restored = spaceFromJSON(JSON.parse(json));
restored.step(1 / 60);
The /serialization entry point is tree-shakeable — it does not pull in the engine
bootstrap when unused. The snapshot captures bodies, shapes, materials, interaction
filters, fluid properties, all constraint types (except UserConstraint), and compounds.
Arbiters and broadphase tree state are reconstructed automatically on the first step.
Record a deterministic simulation and play it back later — same machine, another
machine, days later. Built on top of /serialization plus space.deterministic = true.
import "@newkrok/nape-js";
import { Recorder, Player, encodeReplay, decodeReplay } from "@newkrok/nape-js/replay";
// Record — user supplies the input payload type and the apply function.
space.deterministic = true;
const recorder = new Recorder<MyInput>(space, { keyframeEvery: 60 });
for (let f = 0; f < 600; f++) {
recorder.recordFrame(readUserInput()); // null = no input this frame
applyUserInput(space); // user's own logic
space.step(1 / 60);
}
const replay = recorder.finish();
const blob = encodeReplay(replay); // Uint8Array — store, share, transfer
// Replay — anywhere, including a different machine on the same platform.
const replay2 = decodeReplay<MyInput>(blob);
const player = new Player(replay2, (input, space, frame) => {
if (input.fire) somebody.applyImpulse(new Vec2(0, -200));
});
const sp = player.restore();
while (!player.finished) player.step();
// player.stepTo(150) — random-access scrub via keyframes
The /replay entry point is tree-shakeable. The library is intentionally a thin
layer: it owns the snapshot + input-log plumbing, and the user owns the
applyInput callback. This keeps the replay deterministic as long as the
callback is a pure function of (input, space, frame).
Run physics off the main thread for smooth rendering even with hundreds of bodies.
import "@newkrok/nape-js";
import { PhysicsWorkerManager } from "@newkrok/nape-js/worker";
const mgr = new PhysicsWorkerManager({ gravityY: 600, maxBodies: 256 });
await mgr.init();
const id = mgr.addBody("dynamic", 100, 50, [{ type: "circle", radius: 20 }]);
mgr.start();
// Read transforms on the main thread (zero-copy with SharedArrayBuffer)
function render() {
const t = mgr.getTransform(id);
if (t) drawCircle(t.x, t.y, t.rotation);
requestAnimationFrame(render);
}
render();
Uses SharedArrayBuffer for zero-copy transform sharing when COOP/COEP headers are
present, with automatic postMessage fallback otherwise.
npm install
npm run build # tsup → packages/*/dist/ (ESM + CJS + DTS)
npm test # vitest — 5761 engine tests + 71 pixi-adapter tests
npm run benchmark # Performance benchmarks
Releases are automated: see docs/guides/workflow.md
for the per-package auto-release pipeline. Run
node scripts/ci/release.mjs --dry-run from the repo root to preview what
would publish on the next master merge.
MIT