Chapter IV

Movement

Our landscape is ready, but it's not moving. Now is the time to introduce a new abstraction: the camera.

Train Window

Camera

Imagine that you're on a train on the edge of a forest, looking out the window. What you see is limited by that window.

That's the role of the camera.

To simplify things, let's make the camera exactly the size of the canvas. As a reminder, the canvas now uses our own unit of measurement, which is 7 units wide and 4 units tall.

To begin, let's add the camera to our scene.

→ utils.js
Copy
export default class Scene {

    constructor () {
        this.elements = []
        this.camera = {
            x: 0,
            y: 0,
            width: 7,
            height: 4
        }
    }

    // omitted for brevity
}

Now let's think about our train.

As it moves forward, the view changes, the forest appears to be passing by and the trees seem to be moving backwards.

We need to incorporate this change into our drawScene function.

→ utils.js
Copy
export function drawScene (ctx, scene) {
    scene.elements.forEach(element => {
        const drawParams = Object.assign({}, element)

        drawParams.x -= scene.camera.x
        drawParams.y -= scene.camera.y

        drawImage(ctx, drawParams)
    })
}

By adding a speed property and an update method to our scene, we can control the movement of the camera over time.

The speed is measured in units per second, which means that if it's set to 2 and one second elapses, the camera will move 2 units.

→ scene.js
Copy
export default class Scene {

    constructor () {
        this.elements = []
        this.camera = {
            x: 0,
            y: 0,
            width: 7,
            height: 4,
            speed: 2
        }
    }

    update (deltaTime) {
        this.camera.x += this.camera.speed * deltaTime
    }

    // omitted for brevity

}

Let's move forward in time by one second.

→ toy.js
Copy
scene.update(1)

The scene has successfully moved back by 2 units!

Scene has moved by 2 units

While I'm at it, I'm updating the grid which has a minor sizing issue. Currently, the grid is set to the size of the canvas instead of the camera size, which is causing the problem.

→ utils.js
Copy
export function drawGrid (ctx, {width, height}) {
    const cellSize = 1

    ctx.strokeStyle = 'rgba(0, 0, 0, 0.25)'
    ctx.lineWidth = 0.01

    for (let x = 0; x < width; x += cellSize) {
        ctx.beginPath()
        ctx.moveTo(x, 0)
        ctx.lineTo(x, height)
        ctx.stroke()
    }
    for (let y = 0; y < height; y += cellSize) {
        ctx.beginPath()
        ctx.moveTo(0, y)
        ctx.lineTo(width, y)
        ctx.stroke()
    }
}

Remember to call drawGrid function with the camera as an parameter.

→ toy.js
Copy
drawGrid(ctx, scene.camera)

Animation

To create an animation, the entire scene must be redrawn at every screen refresh, which happens multiple times per second, typically at a rate of 60 frames per second. It's important to note that this frequency can vary between computers.

In JavaScript, the requestAnimationFrame function can be used to trigger a function at the next screen refresh, which allows for smooth animation. By recalling this function within the same function, an animation loop can be created, making it easy to create dynamic animations.

Our upcoming tool will make use of this feature.

→ utils.js
Copy
export function startAnimationLoop (callback) {
    let lastTime = 0

    function animationFrame (time) {
        const deltaTime = time - lastTime
        lastTime = time

        callback(deltaTime / 1000)

        requestAnimationFrame(animationFrame)
    }

    requestAnimationFrame(animationFrame)
}

With that tool, we can update the scene and redraw it at every screen refresh

→ toy.js
Copy
startAnimationLoop(deltaTime => {
    scene.update(deltaTime)
    drawScene(ctx, scene)
})

The current implementation works, but you may have noticed that the scene is being redrawn on top of itself, causing an overlapping effect.

This is because we haven't cleared the screen before redrawing the scene. Let's fix this issue by clearing the screen before drawing the updated scene.

Drawing glitch

The Canvas API offers a clearRect method that can be used to clear a specific area of the canvas.

To make things easier, we will create a utility function to call this method, and use it at the beginning of each new frame to clear the screen before redrawing the updated scene.

→ utils.js
Copy
export function clearCanvas (ctx, {width, height}) {
    ctx.clearRect(0, 0, width, height)
}
→ toy.js
Copy
startAnimationLoop(function (deltaTime) {
    clearCanvas(ctx, scene.camera)
    scene.update(deltaTime)
    drawScene(ctx, scene)
})

Problem solved!

Camera animation

Even more new tools

  • A Camera
  • An animation loop
  • A function to clear the canvas
  • Our scene can now be updated over time

However, you may have noticed that after a few seconds, the scene becomes empty. In the next chapter, we will explore how to generate an infinite world.

Source code
Next: Chapter 5
Infinite world