What are we doing
We are going to focus on properties of the canvas that we are likely to use when creating a 2D game. These are listed below.
- Drawing a background gradient
- Drawing text
- Drawing shapes
- Drawing images
- Clearing the canvas
We will use these properties to create some example art for a scene. You can click the example
button to see the final canvas or keep it a mystery and proceed.
Setup
This project uses the basic-typescript-env
project as a base. Before continuing make sure you have downloaded the simple-typescript-canvas-game-project-setup.
Throughout the project we will build
and run
the project using the commands in the previous basic typescript env
tutorial linked below.
Selecting the canvas context
The canvas
is a HTML
element that we can use to draw on.
The canvas context
is a CanvasRenderingContext2D
that allows us to draw objects onto the canvas.
Add the code below to index.ts
to define references to the canvas and context.
const canvas = document.getElementById('gameCanvas') as HTMLCanvasElement;
const context = canvas.getContext('2d');
canvas.width = 800
canvas.height = 3 * canvas.width / 4.0
The as
keyword allows us to try and cast the element with id gameCanvas
as a HTMLCanvasElement
. If this id
doesn't exist or is not a <canvas>
element we will see an error in the console.
The getContext('2d')
method returns a CanvasRenderingContext2D | null
so before we can do much with the context
we must check that the context
is not null. Add the following lines to index.ts
.
We are also setting the canvas
width to be 800px
and making the height conform to a 4:3
aspect ratio.
const draw = (context: CanvasRenderingContext2D) => {
context.fillRect(0, 0, 100, 100)
}
if (context) {
draw(context)
}
Build and run the app, you should see a black square in the top left corner of the canvas. This indicates that the context is not null
!
Canvas coordinate system
The canvas uses a grid coordinate system with x
and y
coordinates starting at the top left corner of the canvas (position: x: 0, y: 0). The x
coordinates increase right. The y
coordinates increase down.
Clearing the canvas
In order to create the illusion of moving objects that games rely on we require a way of clearing the canvas and re-drawing the objects in their updated positions.
The clearRect
method on the canvas
context
can be used to clear a rectangle with a specified position, width and height of the canvas.
To clear the whole canvas we provide the clearRect
method with the width and height of the canvas. A portion of the canvas can be cleared by adjusting the position, width and height.
To demonstrate how to clear the canvas we will clear the black square we have created.
Add the following line to the index.ts
's draw
method to clear the canvas.
ctx.clearRect(0, 0, canvas.width, canvas.height);
Build and run the app and check that the canvas is blank.
Gradient background
To begin we will give our scene a gradient background.
Add the following drawGradientBackground
method and call it from within the draw
method.
const drawGradientBackground = (context: CanvasRenderingContext2D) => {
let gradient = context.createLinearGradient(0, 0, 0, canvas.height);
gradient.addColorStop(0, 'lightBlue');
gradient.addColorStop(.5, 'blue');
gradient.addColorStop(1, 'lightBlue');
context.fillStyle = gradient;
context.fillRect(0, 0, canvas.width, canvas.height);
}
const draw = (context: CanvasRenderingContext2D) => {
...
drawGradientBackground(context)
}
Here we are creating a vertical gradient. The createLinearGradient
method takes the following arguments, x, xWidth, y, yWidth. For a vertical gradient covering the canvas we specify 0 for horizontal values and 0, height for vertical values.
The addColorStop
method allows us to specify different colours
and the interval where the gradient will have changed to that colour. In our case we are creating a vertical gradient that starts a lightBlue
then from 0
to 50%
(0.5) it transitions into a blue
and then from 50%
to 100%
it transitions back to lightBlue
.
Next we set the fillStyle
to the gradient
we have chosen. This will apply our gradient to any object we draw onto the context using the fill
property.
Finally we use fillRect
to draw a full canvas width and height rectangle of our gradient.
Build and run the app and check that the canvas has a blue gradient background.
Drawing the road
Our scene will have a road that comprises of a gray rectangle and dashed orange line.
const drawRoad = (context: CanvasRenderingContext2D) => {
context.fillStyle = 'gray'
context.fillRect(0, canvas.height - 50, canvas.width, 50)
context.beginPath();
context.lineWidth = 5
context.setLineDash([10, 40]);
context.strokeStyle = 'orange';
context.moveTo(0, canvas.height - 15);
context.lineTo(canvas.width, canvas.height - 15);
context.stroke();
context.setLineDash([])
}
const draw = (context: CanvasRenderingContext2D) => {
...
drawRoad(context)
}
The only new part about this is the orange dashed line. To draw a line on the canvas we need to.
The beginPath
method lets the context know we will be drawing a line.
The lineWidth
method sets the width of the line.
The setLineDash
method ???
The moveTo
method sets a pointer on the context to a particular position.
The lineTo
method moves from where the pointer currently is to the new position.
The stroke
method actually draws the described line.
Setting our constants
To make life easier when drawing a object comprised of multiple shapes, it's best to set up some variables to keep track of the relative positioning and size of the various objects.
Add the following variables to index.ts
.
const carPosX = 200
const baseHeight = 540
const roofHeight = baseHeight - 150
const BonnetHeight = baseHeight - 75
const carWidth = 450
const wheel1CenterX = carPosX + 75
const wheel1CenterY = baseHeight - 15
const wheel2CenterX = carPosX + carWidth - 75
const wheel2CenterY = baseHeight - 15
Drawing car body
Add the following line to index.ts
's draw method.
context.fillStyle = 'red'
context.fillRect(carPosX, BonnetHeight, carWidth, baseHeight - BonnetHeight)
Drawing a circle
const drawFillCircle = (context: CanvasRenderingContext2D, x: number, y: number, radius: number, color: string) => {
context.beginPath();
context.fillStyle = color
context.arc(x, y, radius, 0, Math.PI*2);
context.fill();
context.closePath();
}
The above method will draw a circle filled in with a specified colour.
The arc
method takes the following arguments. x, y position of center of the circle, radius of the circle, starting degrees to final degrees. This allows us to create different kinds of circles such as a semi-circle.
We will use this circle method to draw the wheels on our car next.
Drawing the wheels
Add the following lines to index.ts
's draw method.
drawFillCircle(context, wheel1CenterX, wheel1CenterY, 50, 'black')
drawFillCircle(context, wheel1CenterX, wheel1CenterY, 40, 'gray')
drawFillCircle(context, wheel2CenterX, wheel2CenterY, 50, 'black')
drawFillCircle(context, wheel2CenterX, wheel2CenterY, 40, 'gray')
This will draw the car's two wheels comprised of an outer black and inner gray circle.
Drawing the roof
The roof of our car is a trapezium and uses methods such as moveTo
and lineTo
that we have already seen to form a line path.
Add the following method to index.ts
.
const drawRoof = (context: CanvasRenderingContext2D, color: string, delta: number) => {
context.fillStyle = color
context.beginPath();
context.moveTo(carPosX + 25 + delta, BonnetHeight);
context.lineTo(carPosX + 100, roofHeight + delta);
context.lineTo(carPosX + carWidth - 150, roofHeight + delta);
context.lineTo(carPosX + carWidth - 75 - delta, BonnetHeight);
context.fill()
}
Our roof will be red and will have an inner trapezium to represent the windows.
Add the following lines to index.ts
's draw
method.
drawRoof(context, 'red', 0)
drawRoof(context, '#99f6ff', 15)
To finish lets add a window gap. Add the following method to index.ts
.
const drawWindowGap = (context: CanvasRenderingContext2D) => {
const windowGapCenterX = carPosX + carWidth / 2 - 15
context.fillStyle = 'red'
context.beginPath();
context.moveTo(windowGapCenterX - 5, roofHeight);
context.lineTo(windowGapCenterX + 5, roofHeight);
context.lineTo(windowGapCenterX + 10, BonnetHeight);
context.lineTo(windowGapCenterX - 10, BonnetHeight);
context.closePath();
context.fill()
}
The closePath
method automatically joins where the context position currently is to where it started moveTo
.
Add the following line to index.ts
's draw
method.
drawWindowGap(context)
Drawing an image
It's handy to know how to draw basic geometric shapes onto our canvas but most games require more complicated images to be drawn onto the canvas.
We will be adding some clouds to our scene. Download the cloud-large.png.
Add the following lines to index.ts
let image = new Image();
image.src = "cloud-large.png"
const drawClouds = (context: CanvasRenderingContext2D) => {
context.drawImage(image, 600, 100, 128, 128);
context.drawImage(image, 450, 125, 96, 96);
context.drawImage(image, 600, 175, 128, 128);
}
Add the following line to index.ts
's draw
method.
const draw = (context: CanvasRenderingContext2D) => {
...
drawClouds(context)
}
The drawImage
method takes 5 arguments, an image, positionX, positionY, width and height.
Build and run the app. Do you see any clouds? ... What's going on?
This is because the cloud image hasn't loaded by the time we try to draw it on the canvas.
In general we should wait for the window
to load before interacting with the canvas.
Update index.ts
context check code block to the following.
window.onload = function() {
if (context) {
draw(context)
}
}
We have wrapped our context check and draw method in the window.onload
method. This method will only run once the window has loaded.
Build and run the app again and you should see 3 clouds in the background.
Drawing text
Being able to draw text onto a canvas is crucial to create game dialogue and menu screens etc.
Add the following lines to index.ts
's draw
method to give the scene a title.
const draw = (context: CanvasRenderingContext2D) => {
...
context.font = '80px Arial';
context.fillStyle = 'white';
context.textAlign = "center";
context.fillText("Masterpiece", canvas.width / 2, 75);
}
Drawing the cloud driver
The drawImage
method can take more arguments to allow us to draw only a portion of a particular image onto the canvas. This allows us to work effectively with sprite-sheets and create sprite animation.
If you are not familiar with sprites
and sprite-sheets
I plan to cover using the drawImage
method to create animated sprites
in a future post.
For now add the following line to index.ts
's draw
method to cut out a square portion of the cloud image and place it in the drivers seat.
const draw = (context: CanvasRenderingContext2D) => {
...
context.drawImage(image, 40, 45, 50, 90, 450, 410, 50, 90)
}
Flipping the problem cloud
You may have noticed a problem. One of the clouds appears to be looking the other way. For this scene we want all eyes to be on the car and cloud driver.
To achieve this we will flip the problem cloud vertically.
Update the drawClouds
method in index.ts
with the lines below.
const drawClouds = (context: CanvasRenderingContext2D) => {
context.save();
context.translate(canvas.width, 0);
context.scale(-1, 1);
context.drawImage(image, 600, 100, 128, 128);
context.restore()
context.drawImage(image, 450, 125, 96, 96);
context.drawImage(image, 600, 175, 128, 128);
}
Before drawing the first cloud we are flipping it by using the canvas scale feature. The scale
method allows us to magnify what we draw onto the canvas by some scale factor. We have applied a negative x
scale which flips anything drawn across the vertical axis. We need to also translate the whole context
to see the scaled output.
We are also making use of the save
and restore
context
feature. This allows us to take a snapshot of the context
before we alter it, ie by using the scale
method. The restore
method allows us to reset the context to the state of when it was saved. Using save
and restore
can be cpu intensive and should only be used when necessary (Such as flipping a cloud). Rather than scaling and translating we could of added a new cloud sprite facing the other direction but where's the fun in that.
Finish
I hope this post has given you some ideas about what you can accomplish with the canvas
and context
.
As a bonus challenge make some updates to the scene or create a totally new scene and upload your final masterpiece in the comments below.