Animate Canvas with Generators

Published 9/18/2021 by Colby Rabideau

I've enjoyed working with canvas recently. A couple weeks ago I was making a demo to create the RC logo by drawing squares.

The first version of the code was a bunch of canvas instructions one after the other.

const ctx = canvasElement.getContext("2d")
ctx.fillStyle = "white"
ctx.fillRect(0, 0, canvasElement.width, canvasElement.height)
ctx.fillStyle = "black"
ctx.fillRect(10, 290, 280, 80)
ctx.fillRect(30, 270, 240, 20)
// and so on and so forth...

I wanted a simple animation to illustrate the steps of the program. I split the logic up into an array of functions and called each one in sequence using setInterval.

const instructions = [
  () => {
    ctx.fillStyle = "white"
    ctx.fillRect(0, 0, canvasElement.width, canvasElement.height)
  },
  () => {
    ctx.fillStyle = "black"
    ctx.fillRect(10, 290, 280, 80)
  },
  () => {
    ctx.fillRect(30, 270, 240, 20)
  },
  // ...
]

let step = 0
let intervalId = setInterval(() => {
  if (step < instructions.length) {
    instructions[step]()
    step++
  } else {
    clearInterval(intervalId)
  }
}, 500)

That worked just fine. I guess I had to type "() => {}," a lot but it did the job. Then it occured to me: maybe instead of a list of functions I could use a generator.

Generators

A generator function is a special syntax for creating objects that implement the iterable protocol.

function* makeGenerator() {
  yield 1
  yield 2
  yield 3
}
const generator = makeGenerator()

The yield keyword works like return, but each time we call generator.next() the program starts after the previous yield.

generator.next() // {value: 1, done: false}
generator.next() // {value: 2, done: false}
generator.next() // {value: 3, done: false}

Onces you've made it through all the yields, you can keep calling generate.next() but done will always true and nothing else will happen.

generator.next() // {value: undefined, done: true}
generator.next() // {value: undefined, done: true}
generator.next() // {value: undefined, done: true}

Putting it together

Getting back to the logo animation, I put my canvas instructions into a generator with a yield between each step.

function* makeLogoGenerator() {
  ctx.fillStyle = "white"
  ctx.fillRect(0, 0, canvasElement.width, canvasElement.height)
  yield
  ctx.fillStyle = "black"
  ctx.fillRect(10, 290, 280, 80)
  yield
  ctx.fillRect(30, 270, 240, 20)
  yield
  // ...
}

Then I call logoGen.next() in setInterval, until the generator is done.

const logoGen = makeLogoGenerator()
const intervalId = setInterval(() => {
  if (logoGen.next().done) clearInterval(intervalId)
}, 500)

Calling generator.next() over time creates a simple animation effect on the canvas. It works pretty well!

The working demo is here and the source is on GitHub.