Physics Simulation
Manual gravity, springs, and collision detection — plus an introduction to Matter.js for full rigid-body physics.
Manual physics
Before reaching for a library, it’s valuable to implement basic physics manually. You’ll understand what the library is doing, and manual physics is often faster for simple cases.
Euler integration
The core of most game physics:
// Each frame:
// velocity += acceleration * dt
// position += velocity * dt
// In p5.js, dt is usually 1 (frame-based, not time-based)
velocity.add(acceleration);
position.add(velocity);
acceleration.mult(0); // reset — re-apply forces next frame
Spring simulation
Hooke’s law: force = -k * extension:
class Spring {
constructor(anchorX, anchorY, restLength, stiffness) {
this.anchor = createVector(anchorX, anchorY);
this.restLength = restLength;
this.k = stiffness;
this.damping = 0.85;
}
connect(particle) {
let force = p5.Vector.sub(particle.pos, this.anchor);
let currentLen = force.mag();
let stretch = currentLen - this.restLength;
force.normalize();
force.mult(-this.k * stretch);
particle.applyForce(force);
particle.vel.mult(this.damping);
}
draw(particle) {
stroke(100, 100, 150, 100);
strokeWeight(1);
line(this.anchor.x, this.anchor.y, particle.pos.x, particle.pos.y);
fill(150, 100, 200);
noStroke();
circle(this.anchor.x, this.anchor.y, 8);
}
}
Chain of spring-connected particles:
let particles = [];
let springs = [];
function setup() {
createCanvas(600, 500);
for (let i = 0; i < 10; i++) {
particles.push(new PhysicsParticle(300 + i * 30, 100 + i * 20));
}
particles[0].pinned = true;
for (let i = 0; i < particles.length - 1; i++) {
springs.push(new SpringConnector(particles[i], particles[i + 1], 30, 0.1));
}
}
function draw() {
background(20);
let gravity = createVector(0, 0.5);
for (let p of particles) {
if (!p.pinned) p.applyForce(gravity);
p.update();
}
for (let s of springs) {
s.connect();
s.draw();
}
for (let p of particles) p.draw();
}
Circle-circle collision detection
Check if two circles overlap and resolve the overlap:
function resolveCircleCollision(a, b) {
let diff = p5.Vector.sub(b.pos, a.pos);
let dist = diff.mag();
let minDist = a.r + b.r;
if (dist < minDist && dist > 0) {
// Overlap amount
let overlap = minDist - dist;
// Push apart
let axis = diff.copy().normalize();
a.pos.sub(axis.copy().mult(overlap * 0.5));
b.pos.add(axis.copy().mult(overlap * 0.5));
// Elastic collision — swap velocity components along collision axis
let aSpeed = a.vel.dot(axis);
let bSpeed = b.vel.dot(axis);
a.vel.add(axis.copy().mult(bSpeed - aSpeed));
b.vel.add(axis.copy().mult(aSpeed - bSpeed));
}
}
Matter.js — full rigid body physics
For complex collisions, compound shapes, and constraints, use Matter.js:
<script src="https://cdn.jsdelivr.net/npm/matter-js@0.19.0/build/matter.min.js"></script>
const { Engine, Render, Runner, Bodies, Composite, Mouse, MouseConstraint } = Matter;
let engine, particles = [];
function setup() {
createCanvas(700, 500);
engine = Engine.create();
engine.gravity.y = 1;
// Static floor
let floor = Bodies.rectangle(width / 2, height + 25, width, 50, { isStatic: true });
let wallL = Bodies.rectangle(-25, height / 2, 50, height, { isStatic: true });
let wallR = Bodies.rectangle(width + 25, height / 2, 50, height, { isStatic: true });
Composite.add(engine.world, [floor, wallL, wallR]);
}
function draw() {
background(20);
Engine.update(engine, 1000 / 60);
// Draw all bodies
for (let body of Composite.allBodies(engine.world)) {
drawBody(body);
}
}
function drawBody(body) {
let verts = body.vertices;
fill(body.isStatic ? 50 : color(100, 180, 255));
stroke(80, 120, 200);
strokeWeight(1);
beginShape();
for (let v of verts) vertex(v.x, v.y);
endShape(CLOSE);
}
function mouseClicked() {
// Drop a random shape
let shapes = ['circle', 'box', 'polygon'];
let type = random(shapes);
let x = mouseX, y = mouseY;
let body;
if (type === 'circle') {
body = Bodies.circle(x, y, random(15, 35));
} else if (type === 'box') {
body = Bodies.rectangle(x, y, random(30, 70), random(20, 50));
} else {
body = Bodies.polygon(x, y, floor(random(3, 8)), random(20, 40));
}
Composite.add(engine.world, body);
}
Verlet integration — more stable than Euler
Verlet integration is more numerically stable for constraints and cloth simulations:
class VerletParticle {
constructor(x, y) {
this.pos = createVector(x, y);
this.prev = createVector(x, y);
this.pinned = false;
}
update() {
if (this.pinned) return;
let vel = p5.Vector.sub(this.pos, this.prev);
this.prev.set(this.pos);
this.pos.add(vel);
this.pos.y += 0.3; // gravity
}
constrain(other, restLength) {
let diff = p5.Vector.sub(other.pos, this.pos);
let dist = diff.mag();
if (dist === 0) return;
let correction = diff.mult((dist - restLength) / dist / 2);
if (!this.pinned) this.pos.add(correction);
if (!other.pinned) other.pos.sub(correction);
}
}
Key takeaways
- Euler integration: add acceleration to velocity, velocity to position, reset acceleration each frame
- Hooke’s spring force:
F = -k * (current_length - rest_length) - Circle-circle collision: check distance < sum of radii, push apart proportionally, swap velocity components
- Matter.js handles complex rigid bodies, compound shapes, and constraints with minimal setup
- Verlet integration is more stable than Euler for constrained systems like cloth and ropes