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:
Promise's resolver function - when we call this, the promise will resolveLet'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()
}
}
}
pct gives us what percent (or fraction) of the time for the animation has elapsedpct >= 1) we remove the animation and resolve the promiseWe 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().