The End Product

The preceding chapters leads us to the following sketch of balls bouncing off each other very realistically.

And the whole sketch takes exactly 100 lines of code1.

const SKETCH_WIDTH = 600;
const SKETCH_HEIGHT = 300;
const NUM_BALLS = 8;
const BALLS = [];
function wrap(n, min, max) {
  var divisor = max - min;
  n = n - min;
  var modulo = ((n % divisor) + divisor) % divisor;
  return modulo + min;
}
function areColliding(ballA, ballB) {
  if (ballA.pos.dist(ballB.pos) < ballA.r + ballB.r) return true;
  return false;
}
function elastic_collision(m, n, v) {
  // m = mass1, n = mass2, v = velocity1. Velocity2 MUST BE 0
  var x = v * (m - n) / (m + n);
  var y = v + x;
  return [x, y];
}
function handleCollision(ball1, ball2) {
  // Change to a new FoR in which ball2 is at rest
  var vel_adj = ball2.vel.copy();
  ball1.vel.sub(vel_adj);        
  ball2.vel.sub(vel_adj);
  // Now change to a new FoR in which ball2 is at the origin
  var pos_adj = ball2.pos.copy();
  ball1.pos.sub(pos_adj);         
  ball2.pos.sub(pos_adj);
  // Rotate frame of reference so the point of collision is on x axis and > 0
  var angle_adj = ball1.pos.heading();
  ball1.pos.rotate(-angle_adj);
  ball1.vel.rotate(-angle_adj);
  // Handle the collision in the new FoR
  let [v1f, v2f] = elastic_collision(ball1.mass, ball2.mass, ball1.vel.x);
  ball1.vel.x = v1f;
  ball2.vel.x = v2f;
  // Remove overlap
  ball1.pos.x = ball2.r + ball1.r + 0.01;
  // Switch back to the original FoR
  ball1.pos.rotate(angle_adj);
  ball1.vel.rotate(angle_adj);
  ball2.vel.rotate(angle_adj);    
  ball1.pos.add(pos_adj);
  ball2.pos.add(pos_adj);
  ball1.vel.add(vel_adj);
  ball2.vel.add(vel_adj);
}
class Ball {
  constructor(id, x, y, vx, vy, r) {
    this.pos = createVector(x, y);
    this.vel = createVector(vx, vy);
    this.r = r;
    this.id = id;
    this.mass = PI * sq(r);
    this.color = "grey";
  }
  energy() {
    return 0.5 * this.mass * sq(this.vel.mag());
  }
  momentum() {
    return p5.Vector.mult(this.vel, this.mass/100);
  }
  update() {
    this.pos.add(this.vel);
    var x = wrap(this.pos.x, -this.r, width+this.r);
    var y = wrap(this.pos.y, -this.r, height+this.r);
    this.pos.set(x, y);
  }
  show() {
    push();
    fill(this.color);
    strokeWeight(2);
    circle(this.pos.x, this.pos.y, this.r * 2);
    pop();
  }
}
function restart() {
  for (let i = 0; i < NUM_BALLS; i++)
    BALLS[i] = new Ball(i, i*40, i*40, random(-3,3), random(-1,1), random(15,30));
}
function setup() {
  can = createCanvas(SKETCH_WIDTH, SKETCH_HEIGHT);
  restart();
}
function draw() {
  fill("white");
  rect(0,0,width,height);
  // Move every ball
  for (let ball of BALLS) 
    ball.update();
  // Check for and handle collisions
  for (let i = 0; i < BALLS.length; i++) 
    for (let j = i+1; j < BALLS.length; j++) 
      if (areColliding(BALLS[i], BALLS[j])) 
        handleCollision(BALLS[i], BALLS[j]);
  // Draw every ball
  for (let ball of BALLS) 
    ball.show();
}

At this point we have achieved what we set out to do, but there are many questions I have left unanswered, such as:

  • Inelastic collisions
  • Collisions in three dimensions
  • Optimising the code
  • Collisions between other shapes

In the next chapters of this site I will tackle some of these topics, but we’ve achieved a lot so far with just a few simple functions and an understanding of the laws of conservation of energy and momentum, and the idea of reference frames.

  1. The JavaScript police will probably object to my omission of { } in some of the for and if blocks. It may not be best practice and I think I can make an argument for doing it, but here is not the place :-). ↩︎