Our landscape is ready, but it's not moving. Now is the time to introduce a new abstraction: the camera.
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.
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.
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.
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.
scene.update(1)
The scene has successfully moved back 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.
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.
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.
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
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.
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.
export function clearCanvas (ctx, {width, height}) {
ctx.clearRect(0, 0, width, height)
}
startAnimationLoop(function (deltaTime) {
clearCanvas(ctx, scene.camera)
scene.update(deltaTime)
drawScene(ctx, scene)
})
Problem solved!
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.