# Physics & particles

> Deterministic 2D and 3D rigid-body simulation with Rapier, live performer collision, force fields, and WebGPU compute particles.

framefields includes deterministic rigid-body physics powered by Rapier (compiled to WebAssembly) and high-density GPU particle systems simulated in WebGPU compute shaders.

Physics simulates while the composition is built (or steps frame-by-frame in live mode) and replays as per-frame signals. Frame order does not affect the outcome: frame 80 always renders identically on every machine.

## 2D rigid bodies

```ts
import { Composition, Layer, LayerAnimation, Physics } from "framefields";

const W = 1920, H = 1080, FPS = 30;
const comp = new Composition({ width: W, height: H, fps: FPS, durationFrames: 120, backgroundColor: "#0b0c10" });

const world = await Physics.world2d({ fps: FPS, frames: 120, gravity: 2400 });
world.walls({ width: W, height: H });

const cardLayer = Layer.box({
  width: 320, height: 200, background: "#1f2937", borderRadius: 16,
  position: "absolute", x: W / 2 - 160, y: 100,
});

const card = world.body(cardLayer, { restitution: 0.4, friction: 0.6 });
world.simulate();

// Flash on impact
const flash = Layer.box({ width: W, height: H, background: "#ffffff", opacity: 0 });
flash.animate(LayerAnimation.create().signal("opacity", card.impact, { multiplier: 0.8 }));

comp.add([flash, cardLayer]);
```

`world.body(target, options)` creates a body from any layer with an absolute position (`x`, `y`) and size, or from a plain rectangle `{ x, y, width, height }` (useful for invisible funnels and deflectors).

`world.simulate()` runs the simulation once and automatically binds each body's layer `x`, `y`, and `rotation` to the simulated signals.

### Collider shapes

By default (`shape: "auto"`), the collider is inferred from the layer:

| Layer type | Inferred collider |
| --- | --- |
| `Layer.shape("circle", ...)` | Circle |
| `Layer.box` with `borderRadius` | Rounded box matching corner radius |
| `Layer.shape("polygon" | "star" | "path", ...)` | Convex hull of the SVG path outline |
| `Layer.text` without explicit size | Box measured automatically with `FontManager` |
| `Layer.box` / `Layer.image` / `Layer.video` | Rectangular box |

Force a specific shape with `shape: "box" | "circle" | "hull"` or override dimensions with `size: [width, height]`.

### Body options

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `type` | `"dynamic" | "fixed" | "kinematic"` | `"dynamic"` | Dynamic falls and collides; fixed never moves; kinematic follows a track and pushes others |
| `restitution` | `number` | `0.3` | Bounciness, 0–1 |
| `friction` | `number` | `0.6` | Surface friction, 0–1 |
| `density` | `number` | `1.0` | Mass per unit area. Heavier bodies push lighter ones |
| `velocity` | `[vx, vy]` | `[0, 0]` | Initial launch velocity in px/s |
| `angularVelocity` | `number` | `0` | Initial spin in degrees/second |
| `linearDamping` | `number` | `0` | Air resistance per second |
| `angularDamping` | `number` | `0` | Spin resistance per second |
| `gravityScale` | `number` | `1.0` | Multiplier for world gravity (0 makes body float) |
| `lockRotation` | `boolean` | `false` | Disables rotation so the body remains upright |
| `ccd` | `boolean` | `false` | Continuous collision detection for thin, fast-moving bodies |
| `sensor` | `boolean` | `false` | Passes through bodies without collision while still reporting contacts |
| `startFrame` | `number` | `0` | Simulation frame the body wakes up; holds start pose before |
| `follow` | `{ x?, y?, rotation? }` | — | Kinematic driver: position and rotation each frame |
| `output` | `"signals" | "keyframes" | "none"` | `"signals"` | `"signals"` binds to layer; `"keyframes"` writes plain keyframes; `"none"` leaves layer alone |

## Signals & reactions

Every body produces frame-rate signals sampled at composition time:

```ts
card.x;               // layer top-left X every frame
card.y;               // layer top-left Y every frame
card.rotation;        // continuous degrees (no 360 wrap jump)
card.centerX;         // body center X in px
card.centerY;         // body center Y in px
card.vx;              // velocity X in px/s
card.vy;              // velocity Y in px/s
card.speed;           // speed hypot(vx, vy) in px/s
card.angularVelocity; // degrees/s
card.sleeping;        // 1 when settled, 0 when moving
card.impact;          // exponential decay envelope on collision
```

### Impact & collisions

`body.impact` jumps whenever the body suffers a collision and decays over `impactDecay` frames (default 8 frames). A 1500 px/s change in velocity produces an impact level of 1.0.

`world.impact` produces the loudest impact across all bodies on each frame — perfect for triggering camera shake, lens flares, or audio whooshes.

`world.collisions` and `body.collisions` return an array of first contact events:

```ts
for (const contact of world.collisions) {
  // contact: { frame, body, with, speed }
  console.log(`Hit at frame ${contact.frame} with speed ${contact.speed} px/s`);
}
```

Use `world.collisions` to place impact sound effects (`renderSfx({ type: "impact", ... })`) or particle explosions precisely on contact frames.

## Bounds & joints

Create boundaries with built-in helpers:

```ts
// Ground floor sitting at y: 1000
world.ground({ y: 1000, friction: 0.8, restitution: 0.2 });

// Surrounding walls around the canvas box
world.walls({ width: 1920, height: 1080, thickness: 400, ceiling: true });
```

Connect bodies with joints:

```ts
// Pin to swing around a world point (default: body center)
world.pin(signpost, [960, 200]);

// Hinge two bodies together at a world pivot
world.hinge(bodyA, bodyB, [pivotX, pivotY]);

// Weld bodies rigidly together
world.weld(bodyParent, bodyChild);

// Damped spring between two bodies or body and point
world.spring(pendulum, [960, 100], { frequency: 2, dampingRatio: 0.2, length: 300 });

// Distance rope (slack when closer, taut at max length)
world.rope(weight, [960, 100], { length: 450 });
```

## Driving the simulation

Beyond gravity and collisions, worlds can be influenced by forces, impulses, and spatial fields:

```ts
// Steady acceleration (px/s², mass-independent)
world.force(balloon, { y: -1200 }); // buoyant lift

// Signal-driven acceleration (e.g. wind gusts from audio)
world.force(banner, { x: audioSignal });

// Instant velocity kick at a specific frame
world.impulse(ball, 30, [400, -800]); // kick at frame 30
```

### Force fields

`world.field(callback, options)` is called every solver step for each awake body. The callback receives a `FieldSample2D` (`{ x, y, vx, vy, frame, body }`) and returns an acceleration `[ax, ay]` in px/s²:

```ts
// Radial vortex pulling bodies into center
world.field((s) => {
  const dx = 960 - s.x;
  const dy = 540 - s.y;
  const dist = Math.hypot(dx, dy);
  if (dist < 50 || dist > 600) return null;
  const pull = 1500 / dist;
  // Tangential swirl + inward pull
  return [-dy * pull + (dx / dist) * 200, dx * pull + (dy / dist) * 200];
});
```

## 3D rigid bodies

`Physics.world3d` simulates rigid bodies in 3D for `Layer3D` primitives (`roundedBox`, `sphere`, `cylinder`), extruded text, and 3D models (`Layer.glb`, convex hull):

```ts
import { Layer3D, Physics } from "framefields";

const world = await Physics.world3d({ fps: 30, frames: 150, gravity: 2600 });
world.ground({ y: 1000 });

const box = Layer3D.roundedBox({
  width: 240, height: 160, depth: 32, radius: 12,
  x: 960, y: 200, z: 0,
  material: "glass",
});

const body = world.body(box, {
  restitution: 0.3,
  // Lock rotation to keep cards facing the camera while spinning in the image plane
  lockRotation: { x: true, y: true, z: false },
});

world.simulate();
```

Without a camera layer, the 3D scene's `z = 0` plane maps 1:1 onto canvas pixels. Points from 2D video tracks or layout line up directly in the 3D simulation.

3D bodies expose `x`, `y`, `z`, `rotateX`, `rotateY`, `rotateZ`, `vx`, `vy`, `vz`, `speed`, and `impact` signals.

## Live worlds & performers

Normally, `world.simulate()` computes the entire timeline up front when the composition is constructed.

When bodies interact with performers tracked via `comp.vision(clip)`, use **`world.live(comp)`** instead. Live mode advances the physics simulation frame-by-frame as each composition frame is requested:

```ts
const comp = new Composition({ width: W, height: H, fps: 30, durationFrames: 180 });
const dancer = comp.vision("assets/dancer.mp4");
const person = dancer.person.place({ width: W, height: H, fit: "cover" });

const world = await Physics.world3d({ fps: 30, frames: 180, gravity: 2400 });
world.ground({ y: H });

// Kinematic sphere colliders following dancer's wrists
world.body({ x: 0, y: 0, z: 0, width: 60, height: 60, depth: 60 }, {
  type: "kinematic",
  shape: "sphere",
  follow: {
    x: (f) => person.point("rightWrist", f).x,
    y: (f) => person.point("rightWrist", f).y,
    z: 0,
  },
});

// Live mode: steps as frames render; preview starts instantly without up-front delay
world.live(comp);
```

### Silhouette deflection

The tracked performer's silhouette can deflect rigid bodies using `person.sample(frame, x, y)`:

```ts
world.field((s) => {
  const sample = person.sample(s.frame, s.x, s.y);
  if (sample.alpha <= 0.1) return null;
  // Push outwards along the negative normal gradient
  return [-sample.gx * 8000, -sample.gy * 8000];
});
```

## Keyframe baking

To export plain keyframe animations (inspectable in JSON or usable without the live physics runtime), use `bakeKeyframes` instead of `simulate`:

```ts
world.bakeKeyframes({ tolerance: 0.25 });
```

This applies Ramer–Douglas–Peucker trajectory simplification across each channel, leaving the minimum linear keyframes that stay within `tolerance` pixels of the simulation.

---

## GPU Particles (`Layer.particles`)

`Layer.particles` simulates tens of thousands of particles in a WebGPU compute shader and draws them in a single instanced render call. Positions inside the particle layer are local layer pixels.

```ts
comp.add(Layer.particles({
  width: W, height: H,
  max: 20000,
  seed: 42,
  emitter: {
    shape: "rect",
    x: 0, y: H * 0.6,
    width: W, height: 200,
    rate: 3000,
  },
  lifetime: [4, 6],
  speed: [20, 80],
  direction: -90, spread: 180,
  gravity: 30,
  drag: 1.2,
  noise: { strength: 240, scale: 160, speed: 0.3 }, // divergence-free curl noise
  attractors: [{ x: W / 2, y: H / 2, strength: 1200, radius: 300 }],
  floor: H - 40, bounce: 0.3,
  size: [10, 2], sizeJitter: 0.4,
  shape: "soft",
  color: ["#ffdd00", "#ff5a1f", "#4a1205"],
  blend: "add",
  prewarm: 3, // starts already full
}));
```

### Dynamic particle drivers

Emitter position (`emitter.x`, `emitter.y`), emission `rate`, `gravity`, `drag`, `noise.strength`, attractor positions, and `push` forces can be driven by a static number, a signal, or a per-frame function `(frame: number) => number`.

This allows responsive particle effects:
- **Light painting**: Move a point emitter with a tracked hand `(f) => person.point("rightWrist", f).x`. Births automatically interpolate along the path between frames to avoid gaps.
- **Audio bursts**: Modulate `emitter.rate` with an audio drum signal.
- **Explosions**: Trigger instant bursts at specific frames with `emitter.bursts: [{ frame: 30, count: 5000 }]`.

### Performer interaction

Connect particles directly to tracked performers using `push`:

```ts
const dancer = comp.vision("assets/dancer.mp4");
const person = dancer.person.place({ width: W, height: H, fit: "cover" });

comp.add(Layer.particles({
  width: W, height: H,
  push: {
    person,
    strength: 9000,
    sweep: 120000, // extra force imparted by fast-sweeping limbs
  },
  // ...
}));
```

Layer a video cutout of the same clip (`Layer.video(src).withVision({ mode: "matte" })`) above the particles so they appear behind the performer while being swept aside.

### Particle options reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `max` | `number` | `20000` | Maximum particle capacity. Only uses GPU memory, no cost when idle |
| `emitter.shape` | `"rect" | "point" | "line" | "circle" | "ring"` | `"rect"` | Emitter shape in layer px |
| `emitter.rate` | `PhysicsDriver` | `1000` | Particles emitted per second |
| `emitter.bursts` | `Array<{ frame, count }>` | `[]` | Instant particle bursts at specific frames |
| `lifetime` | `number | [min, max]` | `[2, 4]` | Particle lifetime in seconds |
| `speed` | `number | [min, max]` | `[40, 160]` | Initial launch speed in px/s |
| `direction` | `number` | `-90` | Launch direction angle in degrees (0 = right, -90 = up) |
| `spread` | `number` | `360` | Angular spread around direction in degrees |
| `gravity` | `PhysicsDriver | [gx, gy]` | `0` | Gravitational acceleration in px/s² |
| `drag` | `PhysicsDriver` | `0.4` | Velocity damping per second |
| `noise` | `{ strength?, scale?, speed? }` | — | Curl noise flow for turbulent smoke and fluid drifts |
| `attractors` | `Array<{ x, y, strength, radius? }>` | `[]` | Points that pull (positive strength) or repel (negative strength) |
| `floor` | `number` | — | Floor line in layer px where particles bounce |
| `bounce` | `number` | `0.3` | Bounciness at floor and bouncing bounds |
| `bounds` | `"none" | "kill" | "bounce" | "wrap"` | `"none"` | Boundary condition at layer edges |
| `shape` | `"circle" | "soft" | "square" | "spark"` | `"circle"` | Particle render primitive. `"spark"` streaks along velocity |
| `blend` | `"normal" | "add" | "screen"` | `"normal"` | Blend mode. `"add"` makes glows and light trails build up |
| `color` | `string | string[]` | `"#ffffff"` | Color gradient mapped across particle life |
| `colorBySpeed` | `number` | — | When set, maps color gradient according to speed (px/s to reach final color) |
| `prewarm` | `number` | `0` | Seconds simulated before frame 0 so the scene starts in mid-flight |

## Reference example: Physics dancer

The [`examples/28_physics_dancer`](https://github.com/gatewai-dev/framefields/tree/main/examples/28_physics_dancer) reference film demonstrates 3D rigid-body physics, live vision tracking, and GPU compute particles working together in a single shot:

- **3D glass cards:** `Layer3D.roundedBox({ material: "glass" })` cards thrown in on ballistic arcs that bounce off the performer.
- **Kinematic performer tracking:** Kinematic sphere colliders follow arm and head keypoints from `comp.vision(clip).person`.
- **Performer particle push:** 24,000 embers simulated with `Layer.particles` that part around the dancer's silk dress via `push: { person }`.
- **Live execution:** `world.live(comp)` advances physics as each frame renders, starting the preview immediately.

Watch the render on [YouTube](https://youtu.be/POC7rKnuoE4).
