HTML Canvas Basics

by Peter N. Wood


Previous Step 0: Webaudio Basics
Next Step 2: The Game Loop
Last Updated: 28 Sep, 2017

HTML5 provides a really powerful element: the canvas. These days, you can do a lot of cool things with CSS and pseudo selectors, but if you want to build games in the browser, you'd best learn how to use the canvas.

You could fill your webpage with absolute-position div elements, style them to whatever shape you like, and move them around as you need...right? Yes, you could, but it's not a good idea. HTML elements are designed to be static, or at least not move very often; a game like this would be very slow, and really just a terrible misuse of the Document Object Model (DOM). Those poor divs...

Instead we have the canvas; it was designed for the sort of frequent graphical changes you need to make in a video game. Plus, it saves us a lot of trouble to render anything more complicated than a rectangle or circle.

Another great thing about the canvas API: it's supported by just about every modern browser, and doesn't require you to load any external libraries. Unless your using a really old version of Chrome or Firefox, or Internet Explorer older than 9 (which some people still do), the canvas should work just fine for you.

First, you want to decide what size your canvas will be. You can use css to scale the element to any size you like, but the width and height attributes determine it's internal resolution (this is done automatically using a buffer that stays the same size). For games, I like to center my canvas with a wrapping element as below; this makes it easy to scale the canvas if the browser window is too small for any reason. Also, if the user's browser doesn't support the canvas element any html contained in the canvas will be rendered instead; I recommend something like the following table of browser requirements.

<style type="text/css">
#canvas-wrapper {
  position: fixed;
  margin: -300px auto 0;
  width: 800px;
  height: 600px;
  top: 50%;
  left: 0;
  right: 0;
}
</style>
<main id="canvas-wrapper">
  <canvas id="canvas" width="800" height="600">
    <p>
      Sorry, your browser doesn't meet the requirements for this game
    </p>
    <table>
      <caption>Supported Browsers</caption>
      <thead><tr><th>Browser</th><th>Versions</th></tr></thead>
      <tbody>
        <tr><td>Internet Explorer</td><td>9.0+</td></tr>
        <tr><td>Firefox</td><td>3.6+</td></tr>
        <tr><td>Chrome</td><td>4.0+</td></tr>
        <tr><td>Safari</td><td>4.0+</td></tr>
        <tr><td>Opera</td><td>10.1+</td></tr>
      </tbody>
    </table>
  </canvas>
</main>

Next, we need to get the rendering context from our canvas. If you use jQuery, you have to call getContext('2d') on the DOMElement itself, not the special object jQuery gives you. jQuery isn't very useful for games, so I usually don't bother. We're specifying “2d” because the Canvas API also supports 3D rendering through WebGL.

var context = document.getElementById('canvas').getContext('2d');

There are two main ways to render things on the canvas: vector graphics rendering, and copying from an img tag or another canvas. Vector Graphics (which I'll abbreviate VG) is a way of mathematically rendering simple shapes. It can be more processor intensive than loading an image, but VG images scale well, and transformations like rotation are less intensive than with images. Rendering in VG on the canvas also allows us to contain everything we need for a simple game in a single html file.

Copying image data from another source is a useful way to improve performance, but I'll discuss that in a separate tutorial.

Now that we have the rendering context, we can start drawing stuff. We'll start with a simple rectangle. The context has a bunch of properties used to render things, including stroke and fill colors, so let's specify what colors we want to use first. Any color name, hex, or rgba value that's valid in css will work here too. Then we can call fillRect() and strokeRect() to draw a rectangle with an outline.

I've added a grid to this canvas to better illustrate how things are positioned.

context.fillStyle = '#4E4';
context.strokeStyle = 'crimson';

context.fillRect(100, 40, 200, 120);
context.strokeRect(100, 40, 200, 120);

The canvas uses a coordinate system where the x- and y-axes are centered on the upper left corner of the element; x increases from left to right, y increases from top to bottom, and the upper left corner is at x = 0, y = 0. Our rectangle will be 200 pixels wide, 120 pixels high, with it's upper left corner at 100 pixels from the left edge of the canvas, and 40 pixels from the top.

It's important to note that every time you render, you're drawing over whatever you previously rendered as well as what's already on the canvas. That's why we draw the rectangle outline after the rectangle fill, so we can see it better.

Now lets draw something more complicated. Our rendering context also lets us define a set of points to form a path. Like the rectangle, we can fill, stroke, or both after defining the path. We start with beginPath() and moveTo(x, y) to declare our first point, where “x” is the x coordinate, and “y” is the y coordinate. From there, we use lineTo(x, y) to specify that we want the previous point and the new one to connect in a straight line. Note: if you don't explicitly close the path before you try to fill it, the path will act like you called closePath(), connect the last point to the first, and fill that path.

context.fillStyle = '#FFF';
// An isosceles triangle
context.beginPath();
context.moveTo(200, 100);
context.lineTo(300, 200);
context.lineTo(100, 200);
context.closePath();

context.fill();

// Three lines rendered at once
context.strokeStyle = '#0FF';
context.beginPath();

context.moveTo(300, 500);
context.lineTo(400, 400);

context.moveTo(400, 500);
context.lineTo(500, 400);

context.moveTo(500, 500);
context.lineTo(600, 400);

context.stroke();
context.closePath();

It's important to call closePath() so that we clear out the path and don't draw it again by accident. As shown above, if we're just drawing lines, we can call moveTo() to break up the line into several segments before we render. It's good for performance to call functions like fill() and stroke() as rarely as possible. We can do something similar if we want to fill several objects at once, as long as we make sure the last point of each shape is supposed to connect to the first.

Let's also draw a circle. The arc(x, y, radius, startAngle, endAngle, counterclockwise) function lets us draw a circular arc, where “startAngle” is the starting angle in radians ( between 0 and 2π ), “endAngle” is the ending angle, and “counterclockwise” is an optional boolean that determines draw direction, which is clockwise by default.

// Two triangles
context.fillStyle = '#F00';
context.beginPath();

context.moveTo(100, 100);
context.lineTo(200, 100);
context.lineTo(100, 200);

context.moveTo(700, 500);
context.lineTo(600, 500);
context.lineTo(700, 400);

context.fill();
context.closePath();

// Circle
context.beginPath();
context.arc(400, 125, 32, 0, 2 * Math.PI);
context.fill();
context.closePath();

// Half-Circle
context.beginPath();
context.arc(400, 225, 32, 0, Math.PI);
context.fillStyle = '#00F';
context.fill();
context.closePath();

// Pac Man?
context.beginPath();
context.moveTo(384, 325);
context.arc(400, 325, 32, 0.5, 5.78);
context.fillStyle = '#FF0';
context.fill();
context.closePath();

Notice how you can use moveTo() together with arc() for more complex shapes. All the functions that draw a path can be used in tandem; Check out the MDN documentation below for more details.

There you have it! VG rendering in the canvas is very easy, though more complex shapes can take many lines of code. Next, we'll learn about the game loop.

— Peter N. Wood

Previous Step 0: Webaudio Basics
Next Step 2: The Game Loop

Resources

MDN Canvas API documentation
MDN article on optimizing canvas rendering