Coding ยท ยท

Animating objects

This is a quick guide to animating objects on a canvas, by changing their x and y coordinates. The same method could equally be applied to other properties such as width, height, colour etc. It assumes you've gone through this guide before.

Here's the code we're working towards. Throw it in an animations.html and run it with bun animations.html:

<!doctype html>
<title>Animations</title>
<canvas id="canvas" width="500" height="300" style="border: 1px black solid;"></canvas>
<script>
let ctx = canvas.getContext('2d')

let balls = [
  { x: 5, y: 10 },
  { x: 100, y: 200 }
]

moveBall1()
moveBall2()

async function animate (obj, to, duration) {
  return new Promise(resolve => {
    obj.animate = {
      start: Date.now(),
      duration,
      from: { x: obj.x, y: obj.y },
      to,
      resolve
    }
  })
}

async function moveBall1 () {
  await animate(balls[0], { x: 50, y: 100 }, 1000)
  await animate(balls[0], { x: 250, y: 50 }, 2000)
}

async function moveBall2 () {
  await animate(balls[1], { x: 50, y: 100 }, 1500)
  await animate(balls[1], { x: 400, y: 200 }, 1500)
}

setInterval(() => {
  ctx.clearRect(0, 0, canvas.width, canvas.height)

  // update state
  let now = Date.now()
  for (let ball of balls) {
    if (ball.animate) {
      let pct = Math.min(1, (now - ball.animate.start) / ball.animate.duration)
      let { from, to, resolve } = ball.animate
      ball.x = from.x + (to.x - from.x) * pct
      ball.y = from.y + (to.y - from.y) * pct
      if (pct >= 1) {
        ball.animate = null
        resolve()
      }
  }
}

  // draw state
  for (let ball of balls) {
    ctx.fillRect(ball.x, ball.y, 10, 10)
  }
}, 1000/60)
</script>

... you should see 2 objects moving/animating independent of each other.

Let's look at how it works.

async function animate (obj, to, duration) {
  return new Promise(resolve => {
    obj.animate = {
      start: Date.now(),
      duration,
      from: { x: obj.x, y: obj.y },
      to,
      resolve
    }
  })
}

We call this function with eg animate(balls[0], { x: 50, y: 100 }, 1000) to say we want the ball balls[0] to move from its current location to coordinates { x: 50, y: 100 }, and for it to get there 1000ms from now.

The function returns a Promise, which allows us to await the end of the animation, so that as soon as its finished we can animate to a new location.

In the function, we give the ball object a new property animate which is itself an object with several properties:

Let's also have a look at the code in the draw loop that's moving the objects:

// update state
let now = Date.now()
for (let ball of balls) {
  if (ball.animate) {
    let pct = Math.min(1, (now - ball.animate.start) / ball.animate.duration)
    let { from, to, resolve } = ball.animate
    ball.x = from.x + (to.x - from.x) * pct
    ball.y = from.y + (to.y - from.y) * pct
    if (pct >= 1) {
        ball.animate = null
        resolve()
      }
  }
}

We can chain multiple animations together with:

await animate(balls[0], { x: 50, y: 100 }, 1000)
await animate(balls[0], { x: 250, y: 50 }, 2000)
// ...

... because each one await's the promise to resolve.

We manage to get both objects moving at the same time because when we call:

moveBall1()
moveBall2()

... we don't await moveBall1() before we start moveBall2().