p5.js Session 15: Animation

Drop noLoop() and let draw() repeat: movement, clearing the canvas each frame, and five balls bouncing off the walls.

Every sketch in this series had ended with noLoop() in setup(). It draws once and stops. Take it out and draw() repeats instead, over and over, for as long as the sketch is open.

On its own that changes nothing you can see. The same circle drawn in the same place thirty times a second looks exactly like a circle drawn once. Movement only appears when something is different between one pass and the next, and the simplest thing to make different is a position.

draw() runs over and over

A circle, a position, and two pixels added to that position on every pass. The canvas is painted black once, in setup(), and never touched again.

let x = 40;
let y = 300;
let speed = 2;

function setup() {
  createCanvas(600, 600);
  frameRate(30);
  noStroke();
  background(0);
  fill('#FB8B24');
}

function draw() {
  if (x < width - 40) {
    x = x + speed;
  }
  circle(x, y, 80);
}

function keyPressed() {
  if (key === 'g' || key === 'G') {
    saveGif('ball-trail', 10);
  }
}

That bar is not a ball that moved. It is around 260 separate circles, each drawn two pixels further right than the one before, all still sitting there. It is the clearest picture you will get of what draw() is actually doing.

Clearing the canvas each frame

Move one line and the whole thing changes. background(0) comes out of setup() and goes to the top of draw(), so the canvas is wiped before every circle.

let x = 40;
let y = 300;
let speed = 2;

function setup() {
  createCanvas(600, 600);
  frameRate(30);
  noStroke();
  fill('#FB8B24');
}

function draw() {
  background(0);
  if (x < width - 40) {
    x = x + speed;
  }
  circle(x, y, 80);
}

function keyPressed() {
  if (key === 'g' || key === 'G') {
    saveGif('ball-clear', 10);
  }
}

Which raises the obvious question. What if it did not stop?

Five balls, bouncing

One ball needs an x, a y, and a speed on each axis. Five balls need five of each, so all four go into arrays, the same way the palette did in Session 13.

Five fiery colours, one per ball. One starts in the middle of the canvas, the other four in the corners, and every one of them is handed a random direction to set off in.

let palette;
let x = [];
let y = [];
let xSpeed = [];
let ySpeed = [];
let ballSize = 80;

function setup() {
  createCanvas(600, 600);
  frameRate(30);
  noStroke();
  palette = ['#5F0F40', '#9A031E', '#FB8B24', '#E36414', '#0F4C5C'];
  x = [300, 40, 560, 40, 560];
  y = [300, 40, 40, 560, 560];
  for (let i = 0; i < 5; i++) { xSpeed[i] = randomSpeed(); ySpeed[i] = randomSpeed(); }
}

function randomSpeed() {
  let s = random(1.5, 3.5);
  if (random(1) < 0.5) { s = -s; }
  return s;
}

function draw() {
  background(0);
  let r = ballSize / 2;
  for (let i = 0; i < 5; i++) {
    x[i] = x[i] + xSpeed[i];
    y[i] = y[i] + ySpeed[i];
    if (x[i] < r) { x[i] = r; xSpeed[i] = random(1.5, 3.5); ySpeed[i] = randomSpeed(); }
    if (x[i] > width - r) { x[i] = width - r; xSpeed[i] = -random(1.5, 3.5); ySpeed[i] = randomSpeed(); }
    if (y[i] < r) { y[i] = r; ySpeed[i] = random(1.5, 3.5); xSpeed[i] = randomSpeed(); }
    if (y[i] > height - r) { y[i] = height - r; ySpeed[i] = -random(1.5, 3.5); xSpeed[i] = randomSpeed(); }
    fill(palette[i]);
    circle(x[i], y[i], ballSize);
  }
}

function keyPressed() {
  if (key === 'g' || key === 'G') {
    saveGif('bouncing-balls', 10);
  }
}

A real bounce sends a ball away at the mirror of the angle it arrived at. This one does not. It picks a fresh random direction pointing back into the canvas, which is why the five never settle into a rhythm and why the whole thing reads more like scattering than like billiards. That is a choice rather than a mistake, and it is worth knowing which one you are looking at.

The scripts in this session were written for p5.js 2.3.2, the version the web editor runs by default.


Next session, we hand the controls over to the mouse: sketches that answer back to where the pointer is and where it clicks.