Exploring the HTML5 Canvas Tag Through Video Game Cameras

Will Avery
5 min readNov 23, 2020

--

Link’s Awakening, but worse.
I’m entirely too proud of this.

First things first, this is going to be much easier to follow if you read my previous story about creating a basic game engine with Javascript using the Canvas tag. We’ll be using a lot of code from it to speed this whole process up.

Against my better judgement I’ve forged ahead and started creating a camera system using the drawImage() function built into the context rendering system that HTML5 uses. I’ve restructured a few things to improve the engine, most importantly the array that contained our instances of entities is now an object with unique keys for each object. You can implement this by using the code below.

class Entity {
constructor(x,y) {
this.x = x
this.y = y
this.name = 'Entity"
this.fIndex = `${entityCount}`
functionLoop[this.fIndex] ||= this
entityCount += 1
this.sprite = ''
}
loop(){
}
draw(){
let img = document.getElementById(this.sprite)
ctx.drawImage(img, this.x, this.y)
}
destroy() {
delete functionLoop[this.fIndex]
}
}

This is the code that every Entity needs to inherit. We’re eventually going to add more, but to break it down think of these as lifecycle methods. The constructor method sets the entity up, the loop contains the logic that will be executed by the Entity each clock cycle, the draw method is how the Entity will be drawn to our canvas, and the destroy method completely removes its entry in the functionLoop object, freeing a bit of space in memory. All in all this improves the structure and makes the engine more predictable.

We then need to update our clock cycle function to the following.

function fireClock() {
document.dispatchEvent(clockTick)
count += 1 //We'll don't use this currently, but it's good to have a variable incrementing everyframe for the sake of animation later

ctx.fillStyle = "#000000"
ctx.fillRect(0, 0, displayWidth, displayHeight)
for (const object in functionLoop){
functionLoop[object].loop()
}
for (const object in functionLoop){
functionLoop[object].draw()
}
}

This is a small change, but you may be asking what that displayWidth and displayHeight are doing there. This is where the fun begins. The canvas tag doesn’t actually need to be shown on screen. You can store it in a variable and keep it as a buffer for displaying more complex and dynamic images. Today we’re going to implement a background that scrolls with a player controlled object. First we need to add a div to our HTML file with the class “full-screen.” Then, above the file that contains our fireClock() function we’re going to add the following lines.

let sceneWidth = 320
let sceneHeight = 288
let cameraWidth = 160
let cameraHeight = 144
let cameraX = 0
let cameraY = 0
let displayWidth = 1280
let displayHeight = 1152
const bufferCanvas = document.createElement("canvas")
bufferCanvas.width = sceneWidth
bufferCanvas.height = sceneHeight
const mainCanvas = document.createElement("canvas")
const ctx = bufferCanvas.getContext("2d")
const mainCtx = mainCanvas.getContext("2d")
const screenHolder = document.querySelector("#screen-holder")screenHolder.append(mainCanvas)

Ok. So what’s happening here? Simply put we’re building a buffer that we will draw to our graphics onto and THEN we draw that buffer to the main canvas. The reason being is HTML5’s spiffy drawImage() function. You can give the drawImage() function differing numbers of arguments to have it do neat and useful things. For our purposes we need to focus on the following iteration.

drawImage(image, source X, source Y, source Width, source Height, destination X, destination Y, destination Width, destination Height)

This means we can draw a portion of our buffer canvas to the main screen. Lets add the following line to the end of our fireClock() function.

mainCtx.drawImage(bufferCanvas, cameraX, cameraY, cameraWidth, cameraHeight, 0, 0, displayWidth, displayHeight)

Now we’re using those variables we defined to slice a small rectangle out of our full image and display it on main canvas.

This is a rough example of what to expect.

if we create a background image to display to the buffer we can get create a little test environment to see the effects of our work. First lets add a background image to our project (here’s a fun one). Pop it into the dom via your HTML file and define this image as a const called backGround. Then we add the following line to our fireClock() function just before our first loop.

ctx.drawImage(backGround,0,0)

Now, anytime we draw to the buffer canvas, our sprites will be drawn on top of the background. Now we need a little stooge to walk around. Let’s make a new Player Entity.

class Player extends Entity{
constructor(x,y) {
super(x,y)
this.sprite = //You can use whatever image you want here, I recommend one that is 16x16 pixels as it will scale best. Store the image on the Dom and give it an id that you can place here.
this.name = "player"
this.hsp = 0
this.vsp = 0
}
loop() {
this.hsp = ((right) - (left))
this.vsp = ((down) - (up))
this.x += this.hsp
this.y += this.vsp
if (this.x < 0){this.x = 0}
if (this.x > sceneWidth - 16){this.x = sceneWidth - 16}
if (this.y > sceneHeight - 16}{this.y = sceneHeight - 16}
if (this.y < 0){this.y = 0}
cameraX = this.x - (cameraWidth/2)
cameraY = this.y - (cameraHeight/2)
cameraX < 0 ? cameraX = 0 : null
cameraX > sceneWidth - cameraWidth ? cameraX = sceneWidth - cameraWidth : null
cameraY < 0 ? cameraY = 0 : null
cameraY > sceneHeight - cameraHeight ? cameraY = sceneHeight - cameraHeight : null
}

Now, just before we initiate our clock interval we can just create a new player entity at 16x and 16y and move it around the screen. Behold! A functioning camera!

You can extrapolate from here and think of a ton of cool effects you can do with this simple implementation. You can create parallax layers, or use it for zooming in on a product on a store page. This is an incredibly useful effect even outside of video games.

In our next foray into Javascript game engine design we’re going to implement tilemaps! Be sure to follow if you’re interested. In the mean time consider cloning down a version of my engine here and poking around! Enjoy!

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Will Avery
Will Avery

Written by Will Avery

Student at Flatiron School, Chicago. Former musician, current nerd, former dork.

No responses yet

Write a response