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
Section titled “2D rigid bodies”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 impactconst 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
Section titled “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” |
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
Section titled “Body options”| Option | Type | Default | Description |
|---|---|---|---|
type |
`“dynamic” | “fixed” | “kinematic”` |
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 & reactions
Section titled “Signals & reactions”Every body produces frame-rate signals sampled at composition time:
card.x; // layer top-left X every framecard.y; // layer top-left Y every framecard.rotation; // continuous degrees (no 360 wrap jump)card.centerX; // body center X in pxcard.centerY; // body center Y in pxcard.vx; // velocity X in px/scard.vy; // velocity Y in px/scard.speed; // speed hypot(vx, vy) in px/scard.angularVelocity; // degrees/scard.sleeping; // 1 when settled, 0 when movingcard.impact; // exponential decay envelope on collisionImpact & collisions
Section titled “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:
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
Section titled “Bounds & joints”Create boundaries with built-in helpers:
// Ground floor sitting at y: 1000world.ground({ y: 1000, friction: 0.8, restitution: 0.2 });
// Surrounding walls around the canvas boxworld.walls({ width: 1920, height: 1080, thickness: 400, ceiling: true });Connect bodies with joints:
// Pin to swing around a world point (default: body center)world.pin(signpost, [960, 200]);
// Hinge two bodies together at a world pivotworld.hinge(bodyA, bodyB, [pivotX, pivotY]);
// Weld bodies rigidly togetherworld.weld(bodyParent, bodyChild);
// Damped spring between two bodies or body and pointworld.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
Section titled “Driving the simulation”Beyond gravity and collisions, worlds can be influenced by forces, impulses, and spatial fields:
// 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 frameworld.impulse(ball, 30, [400, -800]); // kick at frame 30Force fields
Section titled “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²:
// Radial vortex pulling bodies into centerworld.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
Section titled “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):
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
Section titled “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:
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 wristsworld.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 delayworld.live(comp);Silhouette deflection
Section titled “Silhouette deflection”The tracked performer’s silhouette can deflect rigid bodies using person.sample(frame, x, y):
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
Section titled “Keyframe baking”To export plain keyframe animations (inspectable in JSON or usable without the live physics runtime), use bakeKeyframes instead of simulate:
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)
Section titled “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.
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
Section titled “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.ratewith an audio drum signal. - Explosions: Trigger instant bursts at specific frames with
emitter.bursts: [{ frame: 30, count: 5000 }].
Performer interaction
Section titled “Performer interaction”Connect particles directly to tracked performers using push:
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
Section titled “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” |
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] |
speed |
`number | [min, max]` | [40, 160] |
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 |
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” |
shape |
`“circle” | “soft” | “square” |
blend |
`“normal” | “add” | “screen”` |
color |
`string | string[]` | "#ffffff" |
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
Section titled “Reference example: Physics dancer”The 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.particlesthat part around the dancer’s silk dress viapush: { person }. - Live execution:
world.live(comp)advances physics as each frame renders, starting the preview immediately.
Watch the render on YouTube.