p5.js Intermediate Article 3

Physics Simulation

Manual gravity, springs, and collision detection — plus an introduction to Matter.js for full rigid-body physics.

⏱ 24 min read physics gravity springs collision Matter.js

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