Let’s explore what the canvas is and draw some shapes.
I moved this to a separate page to keep this article short and so I only have to update it in one place.
You can get all the code from this tutorial on the repository below. Keep in mind there’s also a lot of code included that’s not written here. This is because I added more code to create screenshots/diagrams for this tutorial.
Note: As the focus of this tutorial is not building a project, you don’t need to copy every line exactly. In fact, since we’ll be covering many examples, I encourage you to play with it and make a mess.
Some quick bullet points to introduce you to the canvas.
When working with a canvas there are five steps to get started.
We’ll do steps one and two in HTML/CSS, but you could do it in JavaScript if you prefer.
Steps 1 and 2 for this project Our boilerplate / CodePen template already covered setting up the basic styles and adding a canvas element. For this project, I will change the canvas width/height to be 800x1200 to give us plenty of space.
// index.html
<canvas id="gameCanvas" width="800" height="1200"></canvas>
I also changed the canvas background to be white and added some margin to the bottom.
// styles.css
body {
background: #111;
color: #f8f8f8;
}
canvas {
background: #f8f8f8;
padding: 0;
margin: 0 auto;
margin-bottom: 1rem;
display: block;
}
Steps 3 and 4 Get the canvas element by id, then use it to get the “2d” context.
// index.js
;(function () {
const canvas = document.getElementById('gameCanvas')
const ctx = canvas.getContext('2d')
})()
canvas-starter.js
grab the canvas element and create a 2D context
document.getElementById(‘gameCanvas’) — searches for an HTML element that has the id of gameCanvas
. Once it finds the element, we can then manipulate it with JavaScript.
canvas.getContext() — context is our toolbox of paintbrushes and shapes. The 2D context contains the set of tools we want. If you were working with 3D, you would use WebGL instead.
But wait what’s with the function thingy wrapping all of this?
This is an immediately invoked function expression (IIFE). We use it to prevent our code from leaking out in the global scope. This has several benefits such as preventing players (if this were a game) from accessing your variables directly and prevents your code from colliding with someone else’s code (such as a library or another script on the website). The semicolon is there in case some other code is loaded before ours and it doesn’t have a semicolon at the end.
We’ll talk more about security in a future article, for now, let’s get to drawing stuff.
By default, the coordinate system starts at the top left. (X: 0, Y: 0) Moving down or to the right increases X and Y positions. You can play with the example below on this page.
a grid/coordinate project we’ll be creating
Let’s make use of the 2d context object to draw shapes. Feel free to reference the documentation page at any time.
There will also be a link to each method we use.
Before we draw anything, we want to make sure the DOM (HTML) finished loading to avoid any related errors. To do that, we will make a function to handle the setup process we did above and listen for the DOMContentLoaded event. We will call this function init
(for initialize).
Remember: everything stays inside the IIFE wrapper. I won’t be showing it in the example code from now on. If you ever get lost refer to the completed project here.
// initialize config variables here
let canvas, ctx
// setup config variables and start the program
function init () {
canvas = document.getElementById('gameCanvas')
ctx = canvas.getContext('2d')
}
// wait for the HTML to load
document.addEventListener('DOMContentLoaded', init)
canvas-wait-for-dom.js
wait for DOM to load then run init function
We want our canvas and context variables to be available to all of our functions. This requires defining them up top and then giving them a value in the init
function once the DOM loads. This completes our setup.
Note: A more scalable option is to accept the context variable as an argument to our drawing functions.
Let’s start by creating a square:
function init () {
// set our config variables
canvas = document.getElementById('gameCanvas')
ctx = canvas.getContext('2d')
// outlined square X: 50, Y: 35, width/height 50
ctx.beginPath()
ctx.strokeRect(50, 35, 50, 50)
// filled square X: 125, Y: 35, width/height 50
ctx.beginPath()
ctx.fillRect(125, 35, 50, 50)
}
canvas-basic-rectangles.js
basic squares
Inside of our init
function, we use context2D.beginPath to tell the canvas we want to start a new path/shape. On the next line, we create and draw a rectangle (a square in this case).
There are two different methods we use to draw the path to the screen:
ctx.strokeRect(x, y, width, height) — this creates a “stroked” rectangle. Stroke is the same thing as an outline or border
ctx.fillRect(x, y, width, height) — similar to strokeRect
but this fills in the rectangle with a color
The Result:
stroked square and filled square
There are a few things to note here:
positioning the squares
What if we want to change the color/style? Let’s add a red outlined square that’s also filled with blue.
Position first:
ctx.beginPath()
ctx.strokeRect(200, 35, 50, 50) // plugging in our new position
But wait… we want a fill AND a stroke, does this mean we have to draw two squares? You can draw two squares but we will make use of several other methods instead.
// inside the init function
// filled, outlined square X: 200, Y: 35, width/height 50
ctx.beginPath()
ctx.strokeStyle = 'red'
ctx.fillStyle = 'blue'
ctx.lineWidth = 5
ctx.rect(200, 35, 50, 50)
ctx.fill()
ctx.stroke()
stroke-filled-square.js
create an outline and filled in square
The Result:
a third colorful square
ctx.rect(x, y, width, height) — this is like the other two rectangle methods, but it does not immediately draw it. It creates a path for the square in memory, we then configure the stroke, fill, and stroke width before calling ctx.fill
and/or ctx.stroke
to draw it on screen
ctx.strokeStyle = ‘any valid css color’ — sets the outline/stroke color to any string that works in CSS. i.e. ‘blue’, ‘rgba(200, 200, 150, 0.5)’, etc
ctx.fillStyle = ‘any valid css color’ — same as above but for the fill
ctx.lineWidth = number — sets the stroke width
ctx.fill() — fills the current path
ctx.stroke() — strokes the current path
Drawing any shape always follows these steps:
rect
methodNote: We did not need to use the
_ctx.rect()_
function just to change the colors, we used it because we wanted both a stroke and a fill. You can just as easily set_ctx.fillStyle_
and use_ctx.fillRect()_
Setting the fill or stroke style will cause any shapes created after, to have the same style. For example, if we add a fourth square using the code below it would have the same style as the third square.
// 4th square, uses the same style defined previously
ctx.beginPath()
ctx.rect(275, 35, 50, 50)
ctx.fill()
ctx.stroke()
Result:
4th square, oops same style
Anytime you want to add a fill/stroke to your shapes, explicitly define the styles.
Making use of classes we can create a robust Rectangle object and clean up our code.
Classes are functions that create Objects. We want to create Rectangle objects that contain information about themselves, such as their position, styles, area, and dimensions.
I won’t go into much detail on classes as it’s not required and you can get by with normal functions. Check the MDN documentation page on classes to learn more about them.
// ( still inside the IIFE )
// function to create rectangle objects
class Rectangle {
// you create new Rectangles by calling this as a function
// these are the arguments you pass in
// add default values to avoid errors on empty arguments
constructor (
x = 0, y = 0,
width = 0, height = 0,
fillColor = '', strokeColor = '', strokeWidth = 2
) {
// ensure the arguments passed in are numbers
// a bit overkill for this tutorial
this.x = Number(x)
this.y = Number(y)
this.width = Number(width)
this.height = Number(height)
this.fillColor = fillColor
this.strokeColor = strokeColor
this.strokeWidth = strokeWidth
}
// get keyword causes this method to be called
// when you use myRectangle.area
get area () {
return this.width * this.height
}
// gets the X position of the left side
get left () {
// origin is at top left so just return x
return this.x
}
// get X position of right side
get right () {
// x is left position + the width to get end point
return this.x + this.width
}
// get the Y position of top side
get top () {
// origin is at top left so just return y
return this.y
}
// get Y position at bottom
get bottom () {
return this.y + this.height
}
// draw rectangle to screen
draw () {
// destructuring
const {
x, y, width, height,
fillColor, strokeColor, strokeWidth
} = this
// saves the current styles set elsewhere
// to avoid overwriting them
ctx.save()
// set the styles for this shape
ctx.fillStyle = fillColor
ctx.lineWidth = strokeWidth
// create the *path*
ctx.beginPath()
ctx.strokeStyle = strokeColor
ctx.rect(x, y, width, height)
// draw the path to screen
ctx.fill()
ctx.stroke()
// restores the styles from earlier
// preventing the colors used here
// from polluting other drawings
ctx.restore()
}
}
rectangle-class.js
rectangle class
Classes are optional and if the syntax confuses you, then just think of it as an object with methods and properties. Classes aside, two more canvas functions were introduced in the draw method:
ctx.save() — saves the current styles
ctx.restore() — restores the last saved styles
We use these methods to prevent the issue we saw with the fourth square that had unexpected colors.
Canvas stores styles on a stack structure. When we call ctx.save()
it pushes the current styles onto the stack and calling ctx.restore()
pops it off the stack.
style stack animated
I only saved one set of styles in this animation but you can save as many times as you want and restore the most recent styles. Note that saving does not reset the current styles.
Styles include:
strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, lineDashOffset, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation, font, textAlign, textBaseline, direction, imageSmoothingEnabled
— MDN
We can now start putting this Rectangle class to use:
// create a new rectangle object using the Rectangle class
const mySquare = new Rectangle(400, 85, 50, 50, 'gold')
// now we have data and methods to describe our square
console.log(mySquare)
// Object
// fillColor: "gold"
// height: 50
// strokeColor: ""
// strokeWidth: 2
// width: 50
// x: 450
// y: 100
// area: (...)
// bottom: (...)
// left: (...)
// right: (...)
// top: (...)
// draw the square data to screen
mySquare.draw()
using-rectangle-class.js
create a new square using Rectangle class
The Result:
square drawing from rectangle class
Note: I added a grid for the screenshots. We’ll be making a grid at the end of this section.
We can keep reusing the class to make more squares/rectangles:
// mySquare from earlier...
// const mySquare = new Rectangle(400, 85, 50, 50, 'gold')
// lets use the helper methods to
// draw shapes on the sides of mySquare
const childrenSquares = [
// top side square - align x with mySquare's left side
// align bottom with top of mySquare
new Rectangle(mySquare.left, mySquare.top - 50, 50, 50, 'red'),
// right side square - align x with right side of mySquare
// align top with mySquare top
new Rectangle(mySquare.right, mySquare.top, 50, 50, 'green'),
// bottom square
new Rectangle(mySquare.left, mySquare.bottom, 50, 50, 'blue'),
// left square
new Rectangle(mySquare.left - 50, mySquare.top, 50, 50, 'magenta')
]
// draw all of the child squares by looping over them
childrenSquares.forEach(square => square.draw())
children-squares.js
child squares nonsense
We create an array and populate it with new Rectangles positioned based on the mySquare
object created earlier. Then we loop over the array and call the draw method on every square.
What do we get from all that code?
child squares?
Well… it’s something.
I know this is all boring and tedious but we’ve covered the basics of canvas and you’ve at least seen what a class looks like now. Let’s finish creating more shapes. (it’ll go quicker from now on I promise)
Rectangles are the only pre-defined shape with canvas, we create other shapes on our own. We can use lines to build the foundation of these shapes.
// LINES
// save previous styles & set our current styles
ctx.save()
ctx.strokeStyle = 'blue'
ctx.fillStyle = 'blue'
ctx.lineWidth = 4
// stroked trapezoid
ctx.beginPath()
ctx.moveTo(50, 200) // sets our starting point
ctx.lineTo(100, 200) // create a line from start point to X: 100, Y: 200
ctx.lineTo(90, 180) // create the right side
ctx.lineTo(60, 180) // top side
ctx.closePath() // left side and closes the path
ctx.stroke() // draws it to screen via a stroke
// filled trapezoid
ctx.beginPath()
ctx.moveTo(150, 200) // starting point
ctx.lineTo(200, 200) // bottom side
ctx.lineTo(190, 180) // right side
ctx.lineTo(160, 180) // top side
// no need to closePath, fill automatically closes the path
ctx.fill()
// restore saved styles (for other examples)
ctx.restore()
lines-trapezoids.js
using lines to make trapezoid-like shapes
The Result:
trapezoid things
New Methods:
ctx.moveTo(x, y) — you can think of this as moving our virtual “pen”, we use it to set the starting point for the first line
ctx.lineTo(x, y) — creates a line to X, Y; the starting point is the last position of our “pen”. This allows us to start new lines at the endpoint of previous lines.
ctx.closePath() — when using a stroke we need to call this to draw the final line and close the path. Fill will automatically close the path
Note: If you need curved lines then you can use Bézier curves with the quadratic or cubic bézier curve functions. I’ll cover them in another tutorial to keep this one from becoming too long.
If you ever worked with any kind of text editor similar to Microsoft Word or any of Adobe’s tools, then these options will be familiar to you.
// TEXT
// usual setup
ctx.save()
ctx.strokeStyle = 'red'
ctx.fillStyle = 'black'
// text specific styles
ctx.font = 'bold 16px Monospace'
ctx.textAlign = 'left'
ctx.textBaseline = 'alphabetic'
// draw stroked text to screen
ctx.strokeText('Stroked Text', 50, 250)
// calculate the width of this text using current font/styles
const textWidth = ctx.measureText('Stroked Text').width
// X = previous X position + width + 25px margin
ctx.fillText('Filled Text', 50 + textWidth + 25, 250)
ctx.restore()
canvas-text.js
ctx.strokeText(text, x, y) — creates the text path and strokes it
ctx.fillText(text, x, y) — same as above but fills the text
ctx.font(CSSFontString) — set the font using the CSS font format
ctx.measureText(text) — performs some calculations using the current styles and returns an object with the results, including the calculated width
The rest of the options are self-explanatory or require knowledge of font design, which is outside the scope of this tutorial.
// ARCS / Circles
// usual setup
ctx.save()
ctx.strokeStyle = 'black'
ctx.fillStyle = 'red'
// x, y, radius, startAngle, endAngle, antiClockwise = false by default
ctx.beginPath()
ctx.arc(50, 300, 15, 0, 2 * Math.PI, false) // full circle
ctx.fill()
ctx.stroke()
// half circle counter clockwise
ctx.beginPath()
ctx.arc(100, 300, 15, 0, Math.PI, true)
ctx.fill()
ctx.stroke()
// half circle clockwise
ctx.beginPath()
ctx.arc(150, 300, 15, 0, Math.PI)
ctx.fill()
ctx.stroke()
// pacman like
ctx.beginPath()
ctx.fillStyle = 'gold'
ctx.arc(200, 300, 15, 0.1 * Math.PI, 1.85 * Math.PI)
ctx.lineTo(200, 300)
ctx.fill()
ctx.restore()
canvas-arcs.js
circles / arcs code
resulting circles/arcs
The only new function here is the arc
method.
arc(x, y, radius, startAngle, endAngle, antiClockwise)
X, Y
— defines the position of the center point, not the top left
radius
— the size of the circle/arc
startAngle, endAngle
— I think these are self-explanatory but it’s important to note that these angles are in Radians not degrees.
Math Aside: 1π (Pi) radians is equal to half a circle, 2π gives you a full circle.
Watch this video for more on the math of a circle
As there are no triangle functions, we have to make them ourselves using lines.
// TRIANGLES
// usual setup
ctx.save()
ctx.strokeStyle = 'black'
ctx.fillStyle = 'orangered'
// Filled Triangle
ctx.beginPath()
ctx.moveTo(50, 400) // starting point
ctx.lineTo(50, 350) // left side
ctx.lineTo(100, 400) // hypotenuse / long side
ctx.fill() // closes the bottom side & fills
// stroked triangle
ctx.beginPath()
ctx.moveTo(150, 400) // starting point
ctx.lineTo(200, 400) // bottom side
ctx.lineTo(200, 350) // right side
ctx.closePath() // hypotenuse/long side (remember to close path for strokes!)
ctx.stroke()
ctx.restore()
canvas-triangle.js
Nothing new here. Refer to the sections above if you’re lost. (or ask in the comments)
rect(_, _, _, _)
function take?rect
function, what two methods can draw the rectangle to the screen? (the same functions for any path)lineTo(x,y)
Answers: (links to related MDN pages)
Use what you learned to draw a coordinate plane or grid with X and Y starting at the top left and end where the canvas ends.
Examples
Tips
Solution Don’t worry if you didn’t solve it, this one is challenging.
I started by making a new JS file and loading it instead of the example shapes from earlier.
<!-- index.html -->
<!-- <script src="js/index.js"></script> -->
<script src="js/gridSolution.js"></script>
Add the initial setup to your new JS file.
// gridSolution.js
;(function () {
let canvas, ctx
function init () {
// set our config variables
canvas = document.getElementById('gameCanvas')
ctx = canvas.getContext('2d')
}
document.addEventListener('DOMContentLoaded', init)
})()
gridSolution-initial.js
initial setup for the grid solution
Now, decide if you want to make a reusable Grid class or create something simpler. I will keep it simple for this example solution by using only one function.
// draws a grid
function createGrid () {
// draw a line every *step* pixels
const step = 25
// our end points
const width = canvas.width
const height = canvas.height
// set our styles
ctx.save()
ctx.strokeStyle = 'gray' // line colors
ctx.fillStyle = 'black' // text color
// draw vertical from X to Height
for (let x = 0; x < width; x += step) {
}
// draw horizontal from Y to Width
for (let y = 0; y < height; y += step) {
}
// restore the styles from before this function was called
ctx.restore()
}
function init () {
// set our config variables
canvas = document.getElementById('gameCanvas')
ctx = canvas.getContext('2d')
createGrid()
}
createGrid-starter.js
partial grid solution
Look at the above code, then try to fill in the blanks yourself if you haven’t already solved it. The next snippet will be the complete code.
example solution result
// GRID CHALLENGE SOLUTION
;(function () {
let canvas, ctx
// draws a grid
function createGrid () {
// draw a line every *step* pixels
const step = 50
// our end points
const width = canvas.width
const height = canvas.height
// set our styles
ctx.save()
ctx.strokeStyle = 'gray' // line colors
ctx.fillStyle = 'black' // text color
ctx.font = '14px Monospace'
ctx.lineWidth = 0.35
// draw vertical from X to Height
for (let x = 0; x < width; x += step) {
// draw vertical line
ctx.beginPath()
ctx.moveTo(x, 0)
ctx.lineTo(x, height)
ctx.stroke()
// draw text
ctx.fillText(x, x, 12)
}
// draw horizontal from Y to Width
for (let y = 0; y < height; y += step) {
// draw horizontal line
ctx.beginPath()
ctx.moveTo(0, y)
ctx.lineTo(width, y)
ctx.stroke()
// draw text
ctx.fillText(y, 0, y)
}
// restore the styles from before this function was called
ctx.restore()
}
function init () {
// set our config variables
canvas = document.getElementById('gameCanvas')
ctx = canvas.getContext('2d')
createGrid()
}
document.addEventListener('DOMContentLoaded', init)
})()
grid-example-final-solution.js
final example grid code
Feel free to tweak your grid and save it for future use. I saved the animated one as an NPM package to use with upcoming tutorials.
Now it’s time to put everything you’ve learned to use, here’s your challenge:
Draw a picture using a combination of the shapes we learned. Find some reference images or make something up.
Ideas
Tips
canvas.height / 2
and canvas.width / 2
to get the center X, Y of the canvasWhen you finish share a link to your CodePen / GitHub repository in the comments.
What did you struggle with the most? Or was everything a cakewalk? What could have been different? I would love to hear your feedback in the comments. Otherwise, note what you struggled with and spend more time researching and reviewing that topic.
#html5 #javascript #Canvas