The pack
physics2d
function physics2d(opts: Physics2dOptions = {}): Pack<{ physics: Physics2dApi }, PhysicsTopics, { body: BodyData }, { body?: BodySpec }>
The core on the roster. Bodies are roster objects with a body key; the
physics step reads their x/y/rot (so game code may still move them — a
teleport is adopted), integrates, and writes x/y/rot back before collision.
Classes
World
class World
The 2D physics world: bodies held in typed arrays (indexed by the number add returns), the contacts and joints
between them, and the step that moves them. Units are world units, seconds and radians; y points down.
gravityX: number— Gravity along x in units/s²; default 0.gravityY: number— Gravity along y in units/s², positive is down; default 900.substeps: number— Substeps per step. 4 is plenty for games; 8 for tall stacks.contactHertz: number— Contact stiffness. Keep below a quarter of the substep rate (60 at 4 substeps of 60 Hz) or the solver rings.contactDamping: number— Contact damping ratio (dimensionless; 1 is critical damping); default 10.jointHertz: number— Stiffness, in hertz, of the soft constraints that hold joints together; default 60. As forcontactHertz, keep it below a quarter of the substep rate.jointDamping: number— Damping ratio (dimensionless) of those joint constraints; default 2.restitutionThreshold: number— Approach speed, in units/s, below which a bounce is absorbed; default 60. A body's ownminBounceVelocityoverrides it.sleepVelocity: number— Linear speed, in units/s, below which a body counts as still; default 3.sleepAngular: number— Angular speed, in radians/s, below which a body counts as still; default 0.05.sleepTime: number— Seconds a body must stay still before it may sleep (a body only sleeps if everything it touches can too); default 0.5.maxBias: number— Fastest push-out speed, in units/s, that a deep overlap or a joint error may produce; default 400.linearSlop: number— Tolerance, in world units, when generating polygon contacts: edges and edge ends this close count as parallel or touching; default 0.5.cap: number— Slots allocated in each per-body array; it doubles whenaddneeds more.count: number— Slots used so far: the highest body index plus one, freed slots included. Loop to this and skip bodies for whichaliveis false.x!: Float32Array— Body centre x, in world units. Every per-body array is indexed by the numberaddreturned.y!: Float32Array— Body centre y, in world units.a!: Float32Array— Body angle, in radians.ca!: Float64Array— cos and sin of each body's angle, and the angle they were computed for (trigkeeps them current).sa!: Float64Array— Sine of each body's angle; seeca.vx!: Float32Array— Linear velocity x, in units/s.vy!: Float32Array— Linear velocity y, in units/s.av!: Float32Array— Angular velocity, in radians/s.invM!: Float32Array— Inverse mass; 0 for a static or kinematic body, which no force can move.invI!: Float32Array— Inverse rotational inertia; 0 for a static, kinematic or fixed-rotation body.mass!: Float32Array— Mass; 0 for a static or kinematic body.dens!: Float32Array— Each body's own density, as given: what buoyancy is measured against.minBounceVelocity!: Float32Array— Approach speed, in units/s, below which this body absorbs a bounce; negative (the default, -1) defers torestitutionThreshold.rest!: Float32Array— Restitution: 0 is no bounce, 1 a perfect one; default 0.fric!: Float32Array— Friction coefficient; default 0.6.gscale!: Float32Array— Multiplier on gravity for this body; default 1.ldamp!: Float32Array— Linear damping rate per second; default 0.adamp!: Float32Array— Angular damping rate per second; default 0.05.type!: Uint8Array— Body type:STATIC(0),DYNAMIC(1) orKINEMATIC(2).flags!: Uint8Array— Bit flags: 1 fixed rotation, 2 bullet, 4 awake, 8 alive, 16 sensor.sleep!: Float32Array— Seconds the body has been still; it may sleep once this reachessleepTime.user!: Int32Array— The caller's own number for the body (BodyDef.user), mapping contacts back to it; -1 if none was given.oneWay!: Uint8Array— One-way side: 0 none, 1 up, 2 down, 3 left, 4 right (seeOneWay).surf!: Float32Array— Conveyor speed, in units/s, along the body's surface; 0 for none.ax!: Float32Array— Persistent acceleration along x, in units/s² (thrust, per-body wind).ay!: Float32Array— Persistent acceleration along y, in units/s².dragX!: Float32Array— Deceleration toward zero along x, in units/s², applied whileaxis 0.dragY!: Float32Array— Deceleration toward zero along y, in units/s², applied whileayis 0.maxVx!: Float32Array— Speed cap along x, in units/s; 0 for no cap.maxVy!: Float32Array— Speed cap along y, in units/s; 0 for no cap.category!: Uint32Array— Collision category bits; default 1.mask!: Uint32Array— Category bits this body accepts; default 0xffffffff. Two bodies collide only if each one's mask accepts the other's category.bounds: Bounds | null— The world's boundary (walls or bouncing edges), or null for none.boundsHits: BoundsHit[]— Edge hits in the last step: body index, edge and approach speed. Cleared at every step.effectors: (EffectorDef & { id: number })[]— The active effectors, each with theidthataddEffectorgave it.minx!: Float32Array— Left edge of each body's bounding box, in world units (the box of its bounding circle).miny!: Float32Array— Top edge of each body's bounding box, in world units.maxx!: Float32Array— Right edge of each body's bounding box, in world units.maxy!: Float32Array— Bottom edge of each body's bounding box, in world units.bound!: Float32Array— Radius of each body's bounding circle about its centre, in world units.pieces: Piece[][]— Each body's convex pieces (circles and polygons) in body-local coordinates.contacts: Contact[]— Contacts found in the last step, sensor overlaps included, for the caller to read; rebuilt at every step.joints: (Omit<JointDef, 'length'> & { length: number; id: number; alive: boolean; pn: number; pnx: number; pny: number; lowerImpulse: number; upperImpulse: number; motorImpulse: number; angleImpulse: number; springImpulse: number; axisImpulse: number })[]— Joint records, each with theidthataddJointreturned. A joint withalivefalse is dropped at the next step.constructor(capacity = 256)add(d: BodyDef): number— Add a body and return its index, reusing a freed slot if there is one and growing the arrays if not. With noshapethe body is a box ofwidthbyheight(32 by 32 if omitted).remove(i: number): void— Remove bodyialong with its contacts and joints. Its index is reused by a lateradd. Does nothing if it is already gone.alive(i: number): boolean— Whether bodyiexists: it has been added and not removed.isAwake(i: number): boolean— Whether bodyiis awake rather than asleep.wake(i: number): void— Wake bodyiand reset its sleep timer; does nothing for a removed body.setType(i: number, t: 'static' | 'dynamic' | 'kinematic'): void— Change bodyito 'static', 'dynamic' or 'kinematic' and wake it. A body made dynamic gets back the mass and inertia it was added with (its density's, or its ownmass); a static or kinematic one has none.applyImpulse(i: number, ix: number, iy: number, px = this.x[i]!, py = this.y[i]!): void— Give dynamic bodyian instant change of momentum(ix, iy)(mass times velocity) at world point(px, py), by default its centre; off-centre impulses spin it. Wakes the body; does nothing for a static or kinematic one.applyForce(i: number, fx: number, fy: number, dt: number): void— Apply force(fx, fy)to bodyifordtseconds, as an impulse of force timesdt.teleport(i: number, x: number, y: number, angle = this.a[i]!): void— The caller moved a body by hand (the sprite's x/y changed): adopt it.trig(i: number): void— Makeca[i]andsa[i]the cos and sin of body i's angle. They are computed only when the angle has changed since the last call (a signed zero counts as a change), so a turning body pays for them once a substep rather than once per contact. The values are exactlyMath.cosandMath.sinof the angle, whoever asks.step(dt: number): void— Advance the world bydtseconds: collision detection once, thensubstepssolver passes, then restitution, bullet sweeps, bounds, contact reports and sleeping. Does nothing ifdtis 0 or less.containsPoint(i: number, px: number, py: number): boolean— Whether world point(px, py)is inside any piece of bodyi.addEffector(d: EffectorDef): EffectorDef & { id: number }— Add an effector and return the stored object, now carrying itsid. It is not a copy: changing its fields takes effect on the next step.removeEffector(id: number): void— Remove the effector with thisid; an unknown id is ignored.addJoint(d: JointDef): number— Add a joint and return its id, waking both bodies. Defaults are filled in: adistancejoint'slengthis the present distance between its anchors, the slide axis is normalised, andrefAngleis B's present angle relative to A's.removeJoint(id: number): void— Remove a joint. Both ends WAKE: a body held up by a joint that has gone must fall, and a sleeping one would hang in the air.anchorWorld(i: number, lx: number, ly: number): { x: number; y: number }— A body-local point in WORLD units. Public because a joint's ends are drawable: a rope a game can see is a rope a game can draw.raycast(x: number, y: number, dx: number, dy: number, maxDist: number, filter?: (body: number) => boolean): RayHit | null— The nearest body hit by a ray from(x, y)along(dx, dy)(any length) withinmaxDistworld units, or null.fractionin the hit is the distance as a share ofmaxDist.filterskips the bodies it rejects; sensors are otherwise included.queryAabb(x: number, y: number, w: number, h: number, out: number[] = []): number[]— Bodies whose bounds overlap the rect.contactsOf(i: number, out: Contact[] = []): Contact[]— Contacts touching body i right now.
Types
BodyData
interface BodyData
What h.body presents: the columns plus the verbs. Writable fields are live: the next step adopts them.
vx: number— x velocity, units/s. Written by the step; set it to change it.vy: number— y velocity, units/s. Written by the step; set it to change it.av: number— Angular velocity, rad/s.ax: number— Thrust on x, units/s² — live, like velocity.ay: number— Thrust on y, units/s².dragX: number— Deceleration toward zero on x when not thrusting, units/s².dragY: number— Deceleration toward zero on y when not thrusting, units/s².maxVx: number— Speed cap on |vx|, units/s; 0 is none.maxVy: number— Speed cap on |vy|, units/s; 0 is none.readonly mass: number— Mass from density × area or the spec'smass; 0 for static and kinematic bodies.restitution: number— Bounciness 0..1.minBounceVelocity: number— Live per-body bounce cutoff; -1 inherits the solver threshold.friction: number— Surface friction.gravityScale: number— Multiplier on world gravity.density: number— The body's own density — what BUOYANCY measures the fluid's density against. Live. Default 1.surfaceSpeed: number— Conveyor speed along the surface, units/s — live, so a belt can speed up, stop or reverse. 0 is an ordinary surface.readonly type: number— 0 static, 1 dynamic, 2 kinematic. Change it withphysics.setType.readonly awake: number— 1 awake, 0 asleep. A sleeping body does not move until touched, orwake().impulse(ix: number, iy: number, atX?: number, atY?: number): void— An instant change of momentum (mass × units/s) at world point (atX, atY); default the centre. Off-centre spins. Wakes the body.force(fx: number, fy: number): void— A force applied for one 60 Hz frame (it is divided by mass and scaled by 1/60, whatever the fixed step). Wakes the body.wake(): void— Wake a sleeping body.readonly grounded: boolean— Standing on something? (a contact with normal pointing up into this body)readonly outline: ReadonlyArray<{ kind: 'circle'; x: number; y: number; r: number } | { kind: 'polygon'; points: ReadonlyArray<readonly [number, number]> }>— The collision shape in body-local units, one entry per convex piece: what a debug overlay draws. A capsule is a box and two circles.readonly blocked: { up: boolean; down: boolean; left: boolean; right: boolean }— Which sides are touching something (not a sensor), this step.readonly touching: ReadonlyArray<{ other: Handle | null; nx: number; ny: number; impulse: number; speed: number; load: number }>— Contacts touching this body right now.loadis the sustained force in multiples of this body's own weight (a resting body on the ground reads 1; a crate on a pig reads the mass ratio).
BodySpec
interface BodySpec
What spawn takes under body. Every field is optional; omit shape for a box the size of the sprite.
type?: 'static' | 'dynamic' | 'kinematic'—'dynamic'moves and is pushed;'static'never moves;'kinematic'moves by its own velocity and pushes but is not pushed. Default'dynamic'.shape?: ShapeSpec | 'circle' | 'box' | 'capsule'—'box'the sprite's size (default),'circle'of radius min(w, h) / 2,'capsule'the sprite's size, or a full ShapeSpec in body-local units.density?: number— Mass per unit area; mass = density × shape area. Default 1. Ignored whenmassis given.mass?: number— Total mass, overriding density. Default: from density.restitution?: number— Bounciness 0..1; a pair uses the larger of the two. Default 0.minBounceVelocity?: number— Minimum approach speed for bounce, world units/s. Omit to inherit the solver threshold.friction?: number— Surface friction; a pair uses the geometric mean. Default 0.6.gravityScale?: number— Multiplier on world gravity; 0 floats. Default 1.linearDamping?: number— Fraction of linear velocity lost per second. Default 0.angularDamping?: number— Fraction of angular velocity lost per second. Default 0.05.fixedRotation?: boolean— Never rotates (a character). Default false.bullet?: boolean— Swept against statics so a small fast body never passes through a thin wall. Default false.sensor?: boolean— Overlaps and reports, never pushes: triggers, pickups, zones. Default false.category?: number— Unsigned 32-bit collision category bits. Default 1. Both bodies must accept the other category.mask?: number— Unsigned 32-bit accepted category bits. Default 0xffffffff; 0 disables physical contacts, including sensors. Tag overlap rules remain independent.oneWay?: OneWay— A platform others pass through except from this side ('up' = jump up through it, land on it). Default 'none'.surfaceSpeed?: number— Conveyor belt: things touching move at this tangential speed, units/s. Default 0.ax?: number— Persistent acceleration (thrust) on x, units/s². Default 0.ay?: number— Persistent acceleration (thrust) on y, units/s². Default 0.dragX?: number— Deceleration toward zero on x whenaxis 0, units/s² (v2's "drag"). Default 0.dragY?: number— Deceleration toward zero on y whenayis 0, units/s². Default 0.maxVx?: number— Speed cap on |vx|, units/s; 0 is no cap. Default 0.maxVy?: number— Speed cap on |vy|, units/s; 0 is no cap. Default 0.vx?: number— Initial x velocity, units/s. Default 0.vy?: number— Initial y velocity, units/s. Default 0.av?: number— Initial angular velocity, rad/s. Default 0.
Bounds
interface Bounds
The world's boundary rectangle in world units: bodies are kept inside it, each edge either a wall that stops them or an edge that bounces them.
x: number— Left edge, world units.y: number— Top edge, world units.w: number— Width, world units.h: number— Height, world units.bounce?: { top?: boolean; bottom?: boolean; left?: boolean; right?: boolean }— which edges bounce (reflect velocity); unset edges are walls that stopedges?: { top?: boolean; bottom?: boolean; left?: boolean; right?: boolean }— which edges exist at all; default all
BoundsRecord
interface BoundsRecord
One physics.bounds signal: a body hit an edge of physics.bounds().
handle: Handle— The body that hit.edge: 'top' | 'bottom' | 'left' | 'right'— Which edge.speed: number— Speed into the edge, units/s.
ContactRecord
interface ContactRecord
One physics.contact signal: a touching pair this step. Emitted for every pair with a non-zero impulse, and for every sensor overlap.
a: Handle— The first body of the pair.b: Handle— The second body of the pair.nx: number— Contact normal x, unit length, pointing fromatowardb.ny: number— Contact normal y, unit length, pointing fromatowardb.x: number— Contact point x, world units.y: number— Contact point y, world units.impulse: number— Sustained normal impulse this step — a resting body's own weight shows here.speed: number— Approach speed at impact, units/s — use THIS for thuds and damage.sensor: boolean— An overlap with a sensor body: a trigger, never resolved.
CullRecord
interface CullRecord
A body left the cull rect and was killed. id and the last position — the handle is already dead; respawn from these.
id: string— The dead object's id.x: number— Last x, world units.y: number— Last y, world units.vx: number— Last x velocity, units/s.vy: number— Last y velocity, units/s.
EffectorDef
type EffectorDef = | { kind: 'area'; x: number; y: number; w: number; h: number; fx: number; fy: number; /** force as acceleration (units/s²) when true, else as force */ accel?: boolean; /** Linear velocity damping rate inside the area, per second. Default 0. */ drag?: number } | { kind: 'point'; x: number; y: number; radius: number; strength: number; falloff?: 'constant' | 'linear' | 'quadratic'; /** Linear velocity damping rate inside the radius, per second. Default 0. */ drag?: number } | { kind: 'buoyancy'; x: number; y: number; w: number; h: number; density: number; drag?: number; flowX?: number; flowY?: number }
A field that acts on bodies each step: an area of force (wind, a current), a point that pulls or pushes (a
gravity well, an explosion) within a radius, or a buoyancy pool that floats bodies by their density.
Joint2d
interface Joint2d
A live joint. Every joint verb hands one back, and every field on it is the one the SOLVER reads — write
length and the next step uses it.
That is the whole point of returning an object rather than an id. A grapple reels its rope in, a suspension stiffens over rough ground, a winch pays out: all of them are one number changing every frame, and a joint you can only create and destroy forces a game to tear the joint down and build a new one, which loses the solver's accumulated impulses and shows up as a visible jolt.
readonly id: number— The joint id, forunjoint.readonly alive: boolean— False once it has been removed, or once either body died.length: number— A distance joint's rest length. Live: REEL a rope in by shrinking it.min: number— A rope's slack range, world units. Equal min and max is a rigid rod.max: numberhertz: number— Spring frequency; 0 is rigid. Live.damping: number— Spring damping ratio. Live.readonly taut: boolean— Is the rope PULLING — within half a unit of its limit? A grapple asks this to know a swing is loaded.readonly anchorA: { x: number; y: number }— Where end A is now, world units. Draw the rope between this andanchorB.readonly anchorB: { x: number; y: number }— Where end B is now, world units.motor(speed: number, maxTorque?: number): void— Retune the motor: radians/s for a hinge, units/s for a slider, spin rad/s for a wheel.destroy(): void— Remove it. The bodies separate; the handle goes dead.
OneWay
type OneWay = 'none' | 'up' | 'down' | 'left' | 'right'
Which side of a body other bodies can cross from: 'none' is solid on every side; 'up', 'down', 'left' or 'right' lets bodies pass through except those arriving from that side, so 'up' is a platform you can jump up through and land on.
Physics2dApi
interface Physics2dApi
game.physics. Everything that changes world state is undone when the scene exits.
gravity(x: number, y: number): void— Set world gravity, units/s² (default 0, 900). Wakes every body. Scene-scoped.readonly world: World— The raw simulation, for tests and tools.joint(a: Handle, b: Handle, def: Omit<JointDef, 'a' | 'b'>): Joint2d— Connect two bodies with any joint kind; anchors in each body's local frame. Returns the live joint. Throwsphysics.joint: both objects need a body. Scene-scoped.unjoint(j: Joint2d | number): void— Remove a joint, by handle or by id; unknown ids are ignored.pin(a: Handle, b: Handle, at: { x: number; y: number }, opts?: { limits?: [number, number]; motor?: { speed: number; torque?: number } }): Joint2d— A HINGE at a world point: doors, ragdoll elbows, pinball flippers, powered wheels.motordrives it.rope(a: Handle, b: Handle, opts?: { atA?: { x: number; y: number }; atB?: { x: number; y: number }; length?: number; min?: number; max?: number; hertz?: number; damping?: number }): Joint2d— A ROPE or a rod between two world points: a rigidlength, ormin/maxfor slack, orhertzfor a spring.slider(a: Handle, b: Handle, at: { x: number; y: number }, opts?: { axis?: { x: number; y: number }; limits?: [number, number]; motor?: { speed: number; force?: number } }): Joint2d— A SLIDER along an axis: elevators, pistons, sliding doors. Holds the angle;limitsare world units along the axis.wheel(a: Handle, b: Handle, at: { x: number; y: number }, opts?: { axis?: { x: number; y: number }; hertz?: number; damping?: number; limits?: [number, number]; motor?: { speed: number; torque?: number } }): Joint2d— A WHEEL: it slides along its axis on a SPRING and spins freely — a car's suspension in one joint.weld(a: Handle, b: Handle, at?: { x: number; y: number }): Joint2d— WELD two bodies rigidly: build a compound after the fact, glue a rider to a platform.grab(h: Handle, at: { x: number; y: number }, opts?: { maxForce?: number; hertz?: number; damping?: number }): { target(x: number, y: number): void; release(): void }— GRAB a body and drag it to a world point — the mouse joint. Movetargetevery frame;release()when done. It is deliberately SOFT: a rigid pull would fling a heavy body across the screen the instant the pointer moved, and the force budget is what makes a heavy thing feel heavy to drag.explosion(at: { x: number; y: number }, radius: number, strength: number): void— A radial blast: a mass-independent outward kick on every dynamic body in range, falling off with distance.chain(points: ReadonlyArray<readonly [number, number]> | ReadonlyArray<{ x: number; y: number }>, opts?: { radius?: number; loop?: boolean; friction?: number; restitution?: number; tags?: string[]; layer?: string }): { readonly bodies: ReadonlyArray<Handle>; kill(): void }— CHAIN TERRAIN from a polyline: a hillside, a cave wall, a loop of track. Each segment becomes its OWN static body — a capsule, so consecutive segments overlap at their round ends and a body crossing a join has no notch to catch on. One body per segment rather than one body of many pieces, because a manifold is per PAIR: twenty-five pieces on one body would collapse into a single contact with a single normal, and a ball would slide off the world. Static bodies are free, so this costs nothing and is exactly what the tile collision does with its merged runs.raycast(x: number, y: number, dx: number, dy: number, maxDist: number, filter?: (h: Handle) => boolean): { handle: Handle; x: number; y: number; nx: number; ny: number; dist: number } | null— The nearest body along a ray, withinmaxDistunits, or null.filterdrops bodies it returns false for.distis in units. Emitsdebug.ray.setType(h: Handle, type: 'static' | 'dynamic' | 'kinematic'): void— Change a body's type. Made dynamic again, it has the mass it was made with (its density's or its spec'smass). An object with no body is ignored.bounds(b: Bounds | null): void— World bounds: walls, or per-edge bounce (a Breakout ball: bounce top/left/right, no bottom edge). null removes them. Scene-scoped.cull(rect: { x: number; y: number; w: number; h: number } | false): void— Change the cull rect for this scene (undone on exit);falsedisables.effector(e: EffectorDef): EffectorDef & { id: number }— Force fields: 'area' (wind, a conveyor floor), 'point' (a gravity well; negative strength repels), 'buoyancy' (water). Wakes every body. Scene-scoped. The returned object is the LIVE one the step reads, with itsidon it: writee.fx,e.densityore.flowXand the next step uses it — a belt whose speed is a knob does not remove and re-add itself to change.removeEffector(id: number): void— Remove a force field by id.readonly bodies: number— How many bodies exist.
Physics2dOptions
interface Physics2dOptions
Manifest options.
gravity?: { x: number; y: number }— World gravity, units/s². Default{ x: 0, y: 900 }.substeps?: number— Solver substeps per fixed step. Default 4; more for taller stacks.cull?: { x: number; y: number; w: number; h: number } | false— The cull rect: a DYNAMIC body whose centre leaves it is killed and aphysics.cullsignal says so. A body that rolls off the world otherwise falls forever, its numbers growing until they are not numbers. Default: a box 100,000 units around the origin — an infinity guard. Set a tight one for the level;falsedisables.
PhysicsTopics
interface PhysicsTopics
The signals this pack emits, by topic.
'physics.contact': ContactRecord— A touching pair, per step (capacity 512).'physics.bounds': BoundsRecord— A body hit a world bound (capacity 256).'physics.cull': CullRecord— A body was culled (capacity 64).
ShapeSpec
type ShapeSpec = | { kind: 'circle'; r: number; x?: number; y?: number } | { kind: 'box'; w: number; h: number; x?: number; y?: number; angle?: number } | { kind: 'polygon'; points: ReadonlyArray<readonly [number, number]> | ReadonlyArray<{ x: number; y: number }> } | { kind: 'compound'; shapes: ShapeSpec[] } /** A platformer character: a box with round ends. Glides over tile seams and slope joins. */ | { kind: 'capsule'; w: number; h: number }
Shapes. Everything the solver touches is convex: a circle, or a convex polygon in body-local coordinates with precomputed edge normals. A concave polygon is triangulated (ear clipping) into convex pieces at creation; a box is a 4-gon. Mass and inertia come from the pieces.


