Chapter 16
Drawing on Canvas

Drawing is deception

M.C. Escher, cited by Bruno Ernst in The Magic Mirror of M.C. Escher

Browsers give us several ways to display graphics. The simplest is to use regular DOM elements, and use styles to position and color them. This can get you quite far, as the previous chapter showed. By adding partially transparent background images to the nodes, we can make then look exactly the way we want to. It is even possible to rotate or skew nodes by using the transform style.

But we’d be using the DOM for something that it wasn’t originally designed for. There are things, like drawing a line between arbitrary points, that are extremely awkward to do with regular HTML elements.

There are two alternatives. The first is DOM-based, but utilizing SVG (Scalable Vector Graphics), rather than HTML elements. You can think of SVG as a different dialect for describing documents, one that focuses on shapes rather than text. An SVG document can be embedded inside an HTML document, but also included through an <img> tag.

The second alternative is called a canvas. A canvas is a single DOM element that encapsulates a picture. It provides a programming interface for drawing shapes onto the space taken up by the node. The main difference between a canvas and an SVG picture is that in SVG, the original description of the shapes is preserved, so that they can be moved or resized at any time. Canvas, on the other hand, converts the shapes to pixels (colored dots on a raster) as soon as they are drawn, and does not remember what these pixels represent. The only way to move a shape on a canvas is to clear the canvas (or the area around the shape) and redraw it with the shape in its new position.

SVG

This book will not go into SVG in detail, but I will briefly try to explain how it works. At the end of the chapter, I’ll come back to the trade-offs that must be considered when deciding which drawing mechanism is appropriate for a given application.

This is an HTML document with a simple SVG picture inside of it:

<p>Normal HTML here.</p>
<svg xmlns="http://www.w3.org/2000/svg">
  <circle r="50" cx="50" cy="50" fill="red"/>
  <rect x="120" y="5" width="90" height="90"
        stroke="blue" fill="none"/>
</svg>

The xmlns attribute changes an element (and its children) to a different XML namespace. This namespace, identified by a URL, specifies the dialect that we are currently speaking. The <circle> and <rect> tags, which do not exist in HTML, do have a meaning in SVG—they draw shapes, using the style and position specified by their attributes.

These tags create DOM elements, just like HTML tags. For example, this changes the <circle> element to be colored cyan instead:

var circle = document.querySelector("circle");
circle.setAttribute("fill", "cyan");

The canvas element

Canvas graphics can be drawn onto a <canvas> element. You can give such an element width and height attributes to determine its size, in pixels.

A new canvas is empty, meaning it is entirely transparent, and thus shows up simply as empty space in the document.

The canvas tag is intended to support different styles of drawing. To get access to an actual drawing interface, we first need to create a context, which is an object whose methods provide the drawing interface. There are currently two widely supported drawing styles, "2d" for two-dimensional graphics, and "webgl" for three-dimensional graphics through the OpenGL interface.

This book won’t discuss WebGL. We stick to two dimensions. But if you are interested in three-dimensional graphics, I do encourage you to look into WebGL. It provides a very direct interface to modern graphics hardware, and thus allows you to render even complicated scenes very efficiently—from JavaScript.

A context is created through the getContext method on the <canvas> element.

<p>Before canvas.</p>
<canvas width="120" height="60"></canvas>
<p>After canvas.</p>
<script>
  var canvas = document.querySelector("canvas");
  var context = canvas.getContext("2d");
  context.fillStyle = "red";
  context.fillRect(10, 10, 100, 50);
</script>

After creating the context object, the example draws a red rectangle 100 pixels wide and 50 pixels high, with its top left corner at coordinates (10,10).

Just like in HTML (and SVG), the coordinate system that the canvas uses puts (0,0) at the top left corner, and the positive y axis goes down from there, so (10,10) is ten pixels below and to the right of at corner.

Filling and stroking

In the terminology used by the canvas interface (as well as by SVG), there are two things that can be done with a shape. It can be either filled, meaning its area is given a certain color (or pattern), or it can be stroked, which means a line is drawn along its edge.

The fillRect method fills a rectangle. It takes first the x and y coordinates of the rectangle’s top left corner, then its width, and then its height. A similar method, strokeRect, draws the outline of a rectangle.

Neither of these methods take any parameters beyond the dimensions of the rectangle. The way in which the filling or stroking happens is not determined by an argument to the method (as you might justly expect), but rather by properties of the drawing context object.

Setting fillStyle changes the way shapes are filled. It can be set to a string that specifies a color (any color understood by CSS can also be used here).

The strokeStyle property work similarly, but determines the color used for a stroked line. The width of that line is determined by the lineWidth property, which may contain any positive number.

<canvas></canvas>
<script>
  var cx = document.querySelector("canvas").getContext("2d");
  cx.strokeStyle = "blue";
  cx.strokeRect(5, 5, 50, 50);
  cx.lineWidth = 5;
  cx.strokeRect(135, 5, 50, 50);
</script>

When no width or height attributes are specified, as in the previous example, a canvas element gets a default width of 300 and height of 150 pixels.

Paths

A path is a sequence of lines. The 2d canvas interface’s approach to describing such a path is rather peculiar. It is done entirely through side effects.

<canvas></canvas>
<script>
  var cx = document.querySelector("canvas").getContext("2d");
  cx.beginPath();
  for (var y = 10; y < 100; y += 10) {
    cx.moveTo(10, y);
    cx.lineTo(90, y);
  }
  cx.stroke();
</script>

The example creates a path with a number of horizontal line segments, and then strokes it using the stroke method. Each segment created with lineTo starts at the path’s current position, which is the end of the last segment, unless moveTo was called to go to a new position.

When filling a path (using the fill method), each shape is filled separately. A path can contain multiple shapes—each moveTo motion starts a new one. If the path is not already closed (its start and end are in different positions), a line is added from its end to its start, and the shape enclosed by the resulting line is filled.

<canvas></canvas>
<script>
  var cx = document.querySelector("canvas").getContext("2d");
  cx.beginPath();
  cx.moveTo(50, 10);
  cx.lineTo(10, 70);
  cx.lineTo(90, 70);
  cx.fill();
</script>

This draws a filled triangle. Note that only two of the triangle’s sides are explicitly drawn. The third, from the bottom right corner back to the top, is implied, and won’t be there when you stroke the path.

The closePath method explicitly closes a path by adding an actual line segment back to its start. This segment is drawn when stroking the path.

Curves

A path may also contain curved lines. These are, unfortunately, a bit more involved to draw than straight lines.

The quadraticCurveTo method draws a curve to a given point. To determine the curvature of the line, it is given a control point as well as a destination point. You can imagine this control point as attracting the line, giving it its curve. The line won’t go through the control point. Rather, the direction of the line at its start and end point will be such that it aligns with the line from there to the control point. The following picture illustrates this:

<canvas></canvas>
<script>
  var cx = document.querySelector("canvas").getContext("2d");
  cx.beginPath();
  cx.moveTo(10, 90);
  // control=(60,10) goal=(90,90)
  cx.quadraticCurveTo(60, 10, 90, 90);
  cx.lineTo(60, 10);
  cx.closePath();
  cx.stroke();
</script>

We draw a quadratic curve from the left to the right, with (60,10) as control point, and then draw two line segments, going through that control point and back to the start of the line. The result somewhat resembles a Star Trek insignia. You can see the effect of the control point: the lines leaving the lower corners start off in the direction of the control point, and then curve towards their target.

A similar kind of curve is drawn with bezierCurve. Instead of a single control point, this one has two—one for each end of the line. Here is a similar sketch to illustrates the behavior of such a curve:

<canvas></canvas>
<script>
  var cx = document.querySelector("canvas").getContext("2d");
  cx.beginPath();
  cx.moveTo(10, 90);
  // control1=(10,10) control2=(90,10) goal=(50,90)
  cx.bezierCurveTo(10, 10, 90, 10, 50, 90);
  cx.lineTo(90, 10);
  cx.lineTo(10, 10);
  cx.closePath();
  cx.stroke();
</script>

The two control points specify the direction at both ends of the curve. The further they are away from their corresponding point, the more the curve will “bulge” in that direction.

Such curves can be hard to work with—it is not always clear how to find the control points that provide the shape you are looking for. Sometimes you can find a way to compute them, and sometimes you’ll just have to find a suitable value by trial and error.

Easier to reason about are arcs—fragments of a circle. The arcTo method method takes no less than five arguments. The first four aruments act somewhat like the arguments to quadraticCurveTo—the first pair provide a sort of control point, and the second pair gives the line’s destination. The fifth argument provides the radius of the arc. The method will conceptually create a corner—a line going to the control point and then the destination point—and round its point so that it forms part of a circle with the given radius. It then draws the rounded part, as well as a line from the starting position to the start of the rounded part.

<canvas></canvas>
<script>
  var cx = document.querySelector("canvas").getContext("2d");
  cx.beginPath();
  cx.moveTo(10, 10);
  // control=(90,10) goal=(90,90) radius=20
  cx.arcTo(90, 10, 90, 90, 20);
  cx.moveTo(10, 10);
  // control=(90,10) goal=(90,90) radius=80
  cx.arcTo(90, 10, 90, 90, 80);
  cx.stroke();
</script>

The arcTo method will not draw the line from the end of the rounded part to the goal position, though the word “to” in its name would suggest it does. You can follow up with a call to lineTo with the same goal coordinates to add that part of the line.

To draw a circle, you could use four calls to arcTo (each turning 90 degrees). But the arc method provides a simpler way. It takes a pair of coordinates for the arc’s center, a radius, and then a start and end angle.

Those last two parameters make it possible to draw only a part of circle. The angles are measured in radians, not degrees. This means that a full circle has an angle of 2π (2 * Math.PI, about 6.28). The angle starts counting at the point to the right of the circle’s center, and goes clockwise from there. You can use a start of zero and an end bigger than 2π (say, 7) to draw a full circle.

<canvas></canvas>
<script>
  var cx = document.querySelector("canvas").getContext("2d");
  cx.beginPath();
  // center=(50,50) radius=40 angle=0 to 7
  cx.arc(50, 50, 40, 0, 7);
  // center=(150,50) radius=40 angle=0 to ½π
  cx.arc(150, 50, 40, 0, 0.5 * Math.PI);
  cx.stroke();
</script>

The resulting picture contains a line from the left of the full circle (first call to arc) to the left of the quarter-circle (second call). Like other path drawing methods, a line drawn with arc is connected to the previous path segment by default. You’d have to call moveTo (or start a new path) if you want to avoid this.

Drawing a pie chart

Imagine you’ve just taken a job at EconomiCorp Inc., and your first assignment is to draw a pie chart of their customer satisfaction survey results.

The results variable contains an array of objects that represent the survey responses:

var results = [
  {name: "Satisfied", count: 1043, color: "lightblue"},
  {name: "Neutral", count: 563, color: "lightgreen"},
  {name: "Unsatisfied", count: 510, color: "pink"},
  {name: "No comment", count: 175, color: "silver"}
];

To draw a pie chart, we draw a number of pie slices, made up of an arc, and a pair of lines to the center of that arc. We can compute the angle taken up by each arc by dividing a full circle (2π) by the total number of responses, and then multiplying that number (the angle per response) by the amount of people that picked a given choice.

<canvas width="200" height="200"></canvas>
<script>
  var cx = document.querySelector("canvas").getContext("2d");
  var total = results.reduce(function(sum, choice) {
    return sum + choice.count;
  }, 0);
  // Start at the top
  var currentAngle = -0.5 * Math.PI;
  results.forEach(function(result) {
    var sliceAngle = (result.count / total) * 2 * Math.PI;
    cx.beginPath();
    // center=100,100, radius=100
    cx.arc(100, 100, 100,
    // from current angle, clockwise by slice's angle
           currentAngle, currentAngle + sliceAngle);
    currentAngle += sliceAngle;
    cx.lineTo(100, 100);
    cx.fillStyle = result.color;
    cx.fill();
  });
</script>

But a chart that doesn’t tell us what it means is not very helpful. We would like to have a way to draw text to the canvas.

Text

A 2d canvas drawing context provides the methods fillText and strokeText. The latter can be useful for outlining letters, but usually fillText is what you need. It will fill the given text with the current fillColor.

<canvas></canvas>
<script>
  var cx = document.querySelector("canvas").getContext("2d");
  cx.font = "28px Georgia";
  cx.fillStyle = "fuchsia";
  cx.fillText("I can draw text, too!", 10, 50);
</script>

The size, style, and font of the text can be specified using the font property. The example gives just a font size and family name. You can add italic or bold to the start of the string to select a style.

The last two arguments to fillText (and strokeText) provide the position at which the font is drawn. By default, they indicate the position of the start of the text’s alphabetic baseline (the line on which the letters “stand”, not counting hanging parts in letters like “j” or “p”). The horizontal position can be changed by setting the textAlign property to "end" or "center", the vertical position by setting textBaseline to "top", "middle", or "bottom".

We will come back to our pie chart, and the problem of labeling the slices, in the exercises at the end of the chapter.

Images

In computer graphics, a distinction is often made between vector graphics and bitmap graphics. The first is what we have been doing so far in this chapter—specifying a picture by giving a logical description of shapes. Bitmap graphics, on the other hand, don’t specify actual shapes but rather work with pixel data (rasters of colored dots).

The drawImage method allows us to draw pixel data onto a canvas. This pixel data can originate from an <img> element or from another canvas (neither have to be visible in the actual document). The example below creates a detached <img> element and loads an image file into it. But it can not immediately start drawing from this picture, because the browser may not have fetched it yet. To deal with this, we register a "load" event handler, and do the drawing after the image has loaded.

<canvas></canvas>
<script>
  var cx = document.querySelector("canvas").getContext("2d");
  var img = document.createElement("img");
  img.src = "img/hat.png";
  img.addEventListener("load", function() {
    for (var x = 10; x < 200; x += 30)
      cx.drawImage(img, x, 10);
  });
</script>

By default, drawImage will draw the image at its original size. You can give it two additional arguments to determine the width and height with which it is drawn.

When drawImage is given nine arguments, it can be used to draw only a fragment of an image. The second to fifth argument indicate the rectangle (x, y, width, and height) in the source image that should be copied, and the sixth to ninth argument give the rectangle (on the canvas) into which it should be copied.

This can be used to pack multiple sprites (image elements) into a single image file, and then only draw the part you need. For example, we have this picture containing a game character in multiple poses.

Various poses of a game character

If we alternate which pose we draw, we can show an animation that looks like a walking character.

To animate the picture on a canvas, the clearRect method is useful. It resembles fillRect, but instead of coloring the rectangle, it makes it transparent.

We know that each sprite, each sub-picture, is 24 pixels wide and 30 pixels high. The code below loads the image, and then sets up an interval (repeated timer) to draw the next frame.

<canvas></canvas>
<script>
  var cx = document.querySelector("canvas").getContext("2d");
  var img = document.createElement("img");
  img.src = "img/player.png";
  var spriteW = 24, spriteH = 30;
  img.addEventListener("load", function() {
    var cycle = 0;
    setInterval(function() {
      cx.clearRect(0, 0, spriteW, spriteH);
      cx.drawImage(img,
                   // source rectangle
                   cycle * spriteW, 0, spriteW, spriteH,
                   // destination rectangle
                   0,               0, spriteW, spriteH);
      cycle = (cycle + 1) % 8;
    }, 120);
  });
</script>

The cycle variable tracks our position in the animation. Each frame, it is incremented and then clipped back to the 0 to 7 range by using the remainder operator. This variable is then used to compute the x coordinate that the sprite for the current posture has in the picture.

Transformation

But what if we want our character to walk to the left instead of to the right? We could add another set of sprites, of course. But we can also instruct the canvas to draw the picture the other way round.

Calling the scale method will cause anything drawn after it to be scaled. It takes two parameters, one to set a horizontal scale and one to set a vertical scale.

<canvas></canvas>
<script>
  var cx = document.querySelector("canvas").getContext("2d");
  cx.scale(3, .5);
  cx.beginPath();
  cx.arc(50, 50, 40, 0, 7);
  cx.lineWidth = 3;
  cx.stroke();
</script>

Scaling will cause everything about the drawn image, including the line width, to be stretched out or squeezed together as specified. Scaling by a negative amount will flip the picture around. The flipping happens around point (0,0), which means that it will also flip the direction of the coordinate system. When a horizontal scaling of -1 is applied, a shape drawn at x position 100 will end up at what used to be position -100.

So to turn a picture around, we can not simply add cx.scale(-1, 1) before the call to drawImage, since that would move our picture outside of the canvas, where it won’t be visible.

One way to fix this is by adjusting the coordinates given to drawImage to compensate for this (by drawing it at x position -50 instead of 0). Another way, which doesn’t require the code that does the drawing to know about the scale change, is to adjust the axis around which the scaling happens.

To be able to do that, you should know that there are several other methods, besides scale that influence the coordinate system for a canvas. It can be rotated with the rotate method, and moved with the translate method. The interesting— and confusing—thing is that these transformations stack, meaning that each one happens relative to the previous transformations.

So if we translate (move) by 10 horizontal pixels twice, everything will be drawn 20 pixels to the right. If we first move the center of the coordinate system to (50,50) and then rotate by 20 degrees (0.1π in radians), that rotation will happen around point (50,50).

Stacking transformations

But if we first rotated by 20 degrees, and then translated by (50,50), the translation will happen in the rotated coordinate system, and thus produce a different orientation. The order in which transformations are applied matters.

To flip a picture around the vertical line at a given x position, we can do the following:

function flipHorizontally(context, around) {
  context.translate(around, 0);
  context.scale(-1, 1);
  context.translate(-around, 0);
}

We first move the y axis to where we want our mirror to be, then apply the mirroring, and finally move the y axis back to its proper place in the mirrored universe. The picture below tries to explain why this works.

Mirroring around a vertical line

This shows the coordinate systems before and after mirroring in the central line. If we draw a triangle at a positive x position, it would, by default, be in the place where triangle 1 is. A call to flipHorizontally first does a translation to the right, which gets us to triangle 2. It then scales, flipping the triangle back to position 3. This is not where it should be, if it were mirrored in the given line. The second translate call fixes this—it “cancels” the initial translation, and makes triangle 4 appear exactly where it should.

We can now draw a mirrored character at position (100,0) by flipping the world around the character’s vertical center:

<canvas></canvas>
<script>
  var cx = document.querySelector("canvas").getContext("2d");
  var img = document.createElement("img");
  img.src = "img/player.png";
  var spriteW = 24, spriteH = 30;
  img.addEventListener("load", function() {
    flipHorizontally(cx, 100 + spriteW / 2);
    cx.drawImage(img, 0, 0, spriteW, spriteH,
                 100, 0, spriteW, spriteH);
  });
</script>

Storing and clearing transformations

Transformations stick around. Everything else we draw after drawing that mirrored character would also be mirrored. That might be a problem.

It is possible to save the current transformation, do some drawing and transforming, and then restore the old transformation. This is usually the proper thing to do for a function that needs to temporarily transform the coordinate system: first save whatever the code that called the function was using, then do its thing (on top of the existing transformation), and then revert to what it started with.

The save and restore methods on the 2d canvas context perform this kind of transformation management. They conceptually keep a stack of transformation states. When you call save, the current state is pushed onto the stack, and when you call restore, the state on top of the stack is taken off, and used as the context’s current transformation.

The branch function in the example below illustrates what you can do with a function that changes the transformation and then calls another function (in this case itself), which continues drawing with the given transformation.

This function draws a tree-like shape by first drawing a line, and then moving the coordinate system to the end of the line and calling itself twice, first rotated to the left, and then rotated to the right. Every call reduces the length of the branch drawn, and the recursion stops when the length drops below 8.

<canvas width="600" height="300"></canvas>
<script>
  var cx = document.querySelector("canvas").getContext("2d");
  function branch(length, angle, scale) {
    cx.fillRect(0, 0, 1, length);
    if (length < 8) return;
    cx.save();
    cx.translate(0, length);
    cx.rotate(-angle);
    branch(length * scale, angle, scale);
    cx.rotate(2 * angle);
    branch(length * scale, angle, scale);
    cx.restore();
  }
  cx.translate(300, 0);
  branch(60, 0.5, 0.8);
</script>

If the calls to save and restore were not there, the second recursive call to branch would end up with the position and rotation created by the first call—it would not be connected to the current branch, but rather to the innermost, rightmost branch drawn by the first call. The resulting shape might also be interesting, but it is definitely not a tree.

Back to the game

We now know enough about canvas drawing to start working on the canvas-based display system for the game from the previous chapter. The new display will no longer be showing just colored boxes. Instead, we’ll use drawImage to draw pictures that represent the game’s elements.

We will define an object type CanvasDisplay, supporting the same interface as DOMDisplay from Chapter 15—namely the methods drawFrame and clear.

This object keeps a little more information that DOMDisplay. Rather than using the scroll position of its DOM element, it tracks its own viewport, which tells us what part of the level we are currently looking at. It also tracks time, and uses that to decide which animation frame to use. And finally, it keeps a flipPlayer property, so that even when the player is standing still, it keeps facing the direction it last moved in.

function CanvasDisplay(parent, level) {
  this.canvas = document.createElement("canvas");
  this.canvas.width = 600;
  this.canvas.height = 450;
  parent.appendChild(this.canvas);
  this.cx = this.canvas.getContext("2d");

  this.level = level;
  this.animationTime = 0;
  this.flipPlayer = false;

  this.viewport = {
    left: 0,
    top: 0,
    width: this.canvas.width / scale,
    height: this.canvas.height / scale
  };

  this.drawFrame(0);
}

CanvasDisplay.prototype.clear = function() {
  this.canvas.parentNode.removeChild(this.canvas);
};

The animationTime counter is the reason we passed the step size to drawFrame in Chapter 15, even though DOMDisplay does not use it. Our new drawFrame function uses it to track time, so that it can switch between animation frames based on the current time.

CanvasDisplay.prototype.drawFrame = function(step) {
  this.animationTime += step;

  this.updateViewport();
  this.clearDisplay();
  this.drawBackground();
  this.drawActors();
};

Other than tracking time, the method updates the viewport for the current player position, fills the whole canvas with a background color, and draws the background and actors onto that. Note that this is different from the approach in Chapter 15, where we drew the background once and scrolled the wrapping DOM element to move it.

Because shapes on a canvas are just pixels, after we draw them, there is no way to move them (or remove them). The only way to update the canvas display is to clear it and redraw the scene.

The updateViewport method is very similar to DOMDisplay's scrollPlayerIntoView method. It checks whether the player is too close to the edge of the screen, and moves the viewport when this is the case.

CanvasDisplay.prototype.updateViewport = function() {
  var view = this.viewport, margin = view.width / 3;
  var player = this.level.player;
  var center = player.pos.plus(player.size.times(0.5));

  if (center.x < view.left + margin)
    view.left = Math.max(center.x - margin, 0);
  else if (center.x > view.left + view.width - margin)
    view.left = Math.min(center.x + margin - view.width,
                         this.level.width - view.width);
  if (center.y < view.top + margin)
    view.top = Math.max(center.y - margin, 0);
  else if (center.y > view.top + view.height - margin)
    view.top = Math.min(center.y + margin - view.height,
                        this.level.height - view.height);
};

The calls to Math.max and Math.min are used to ensure that the viewport does not end up showing space outside of the level. Math.max(x, 0) has the effect of ensuring the resulting number is not less than zero. Math.min, similarly, ensures a value stays below a given bound.

When clearing the display, we’ll use a slightly different color depending on whether the game is won (brighter) or lost (darker).

CanvasDisplay.prototype.clearDisplay = function() {
  if (this.level.status == "won")
    this.cx.fillStyle = "rgb(68, 191, 255)";
  else if (this.level.status == "lost")
    this.cx.fillStyle = "rgb(44, 136, 214)";
  else
    this.cx.fillStyle = "rgb(52, 166, 251)";
  this.cx.fillRect(0, 0,
                   this.canvas.width, this.canvas.height);
};

To draw the background, we run through the tiles that are visible in the current viewport, using the same trick used in obstacleAt in the previous chapter.

var otherSprites = document.createElement("img");
otherSprites.src = "img/sprites.png";

CanvasDisplay.prototype.drawBackground = function() {
  var view = this.viewport;
  var xStart = Math.floor(view.left);
  var xEnd = Math.ceil(view.left + view.width);
  var yStart = Math.floor(view.top);
  var yEnd = Math.ceil(view.top + view.height);

  for (var y = yStart; y < yEnd; y++) {
    for (var x = xStart; x < xEnd; x++) {
      var tile = this.level.grid[y][x];
      if (tile == null) continue;
      var screenX = (x - view.left) * scale;
      var screenY = (y - view.top) * scale;
      var tileX = tile == "lava" ? scale : 0;
      this.cx.drawImage(otherSprites,
                        tileX,         0, scale, scale,
                        screenX, screenY, scale, scale);
    }
  }
};

Tiles that are not empty (null) are drawn with drawImage. The otherSprites image contains the pictures used for elements other than the player. It contains, from left to right, the wall tile, the lava tile, and then the sprite for a coin.

Sprites for our game

Background tiles are 20 by 20 pixels, since we will use the same scale that we used in DOMDisplay. Thus, the offset for lava tiles is 20 (the value of the scale variable), and the offset for walls is zero.

We do not bother waiting for the sprite image to load in this program. Calling drawImage with an image that hasn’t been loaded yet will simply do nothing. Thus, we might fail to draw the game properly for the first few frames, while the image is still loading, but that is not a serious problem. Since we keep updating the screen, the correct scene will appear as soon as the loading finishes.

The walking character we used before will be used to represent the player. The code that draws it needs to pick the right sprite and direction based on the player’s current motion. The first 8 sprites contain a walking animation. When the player is moving along a floor, we cycle through them based on the display’s animationTime property. This is measured in seconds, and we want to switch frames twelve times per second, so the time is multiplied by 12 first. When the player is standing still, we draw the ninth sprite. During jumps (when the vertical speed is not zero), we use the tenth, rightmost sprite.

Because the sprites are slightly wider than the player object—24 instead of 16 pixels, to allow some space for feet and arms—the method has to adjust the x coordinate and width by a given amount (playerXOverlap).

var playerSprites = document.createElement("img");
playerSprites.src = "img/player.png";
var playerXOverlap = 4;

CanvasDisplay.prototype.drawPlayer = function(x, y, width,
                                              height) {
  var sprite = 8, player = this.level.player;
  width += playerXOverlap * 2;
  x -= playerXOverlap;
  if (player.speed.x != 0)
    this.flipPlayer = player.speed.x < 0;

  if (player.speed.y != 0)
    sprite = 9;
  else if (player.speed.x != 0)
    sprite = Math.floor(this.animationTime * 12) % 8;

  this.cx.save();
  if (this.flipPlayer)
    flipHorizontally(this.cx, x + width / 2);

  this.cx.drawImage(playerSprites,
                    sprite * width, 0, width, height,
                    x,              y, width, height);

  this.cx.restore();
};

The function above is called by drawActors, which is responsible for drawing the all the actors in the game.

CanvasDisplay.prototype.drawActors = function() {
  this.level.actors.forEach(function(actor) {
    var width = actor.size.x * scale;
    var height = actor.size.y * scale;
    var x = (actor.pos.x - this.viewport.left) * scale;
    var y = (actor.pos.y - this.viewport.top) * scale;
    if (actor.type == "player") {
      this.drawPlayer(x, y, width, height);
    } else {
      var tileX = (actor.type == "coin" ? 2 : 1) * scale;
      this.cx.drawImage(otherSprites,
                        tileX, 0, width, height,
                        x,     y, width, height);
    }
  }, this);
};

When drawing something that is not the player, we look at its type to find the offset of the correct sprite. The lava tile is found at offset 20, and the coin sprite at 40 (two times scale).

We have to subtract the viewport’s position when computing the actor’s position, since (0,0) on our canvas corresponds to the top left of the viewport, not the top left of the level. We could also have used translate for this. Either way works.

The tiny document below plugs the new display into runGame:

<body>
  <script>
    runGame(GAME_LEVELS, CanvasDisplay);
  </script>
</body>

Choosing a graphics interface

Whenever you need to generate graphics in the browser, you can choose between plain HTML, SVG, and canvas. There is no single best approach that works in all situations. Each option has strengths and weaknesses.

Plain HTML has the advantage of being simple. It also integrates well with text. Both SVG and canvas allow you to draw text, but won’t help with positioning that text, or wrapping it when it takes up more than one line. In an HTML-based picture, it is very easy to include blocks of text.

SVG can be used to produce crisp graphics that look good at any zoom level. It is more difficult to use than plain HTML, but also much more powerful.

Both SVG and HTML build up a data structure (the DOM) that represents the picture. This makes it possible to modify elements after they are drawn. If you need to repeatedly change a small part of a big picture, in response to what the user is doing or as part of an animation, doing it in canvas can be needlessly expensive. The DOM also allows us to register mouse event handlers on every element in the picture (even on shapes drawn with SVG). This can not be done with canvas.

But canvas’ pixel-oriented approach can be an advantage when drawing a huge amount of tiny elements. The fact that it does not build up a data structure, but only repeatedly draws onto the same pixel surface, cause it to have a lower cost per shape.

There are also effects, such as rendering a scene one pixel at a time (for example using a ray tracer), or post-processing an image with JavaScript (blurring or distorting it) that can only be realistically handled by a pixel-based technique.

In some cases, it is worthwhile to combine several of these techniques. For example, drawing a graph with SVG or canvas, but showing textual information by positioning an HTML element on top of the picture.

For non-demanding applications, it really does not matter much which interface you choose. The second display we built for our game in this chapter could have been implemented using any of these thee graphics technologies, since it does not need to draw text, handle mouse interaction, or work with an extraordinarily large amount of elements.

Summary

In this chapter, we discussed techniques for drawing graphics in the browser, focusing on the <canvas> element.

A canvas node represents an area in a document that our program may draw on. This drawing is done through a drawing context object, created with the getContext method.

The 2d drawing interface allows us to fill and stroke various shapes. The way shapes are filled is determined by the context’s fillStyle property. The way lines are drawn is controlled by the strokeStyle and lineWidth properties.

Rectangles and pieces of text can be drawn with a single method call (fillRect and strokeRect, or fillText and strokeText for text). To create custom shapes, we must first build up a path.

Calling beginPath starts a new path. A number of other methods add lines and curves to the current path, for example lineTo can be used to add a straight line. When a path is finished, it can be filled with the fill method or stroked with the stroke method.

Moving pixels from an image or another canvas onto our canvas is done with the drawImage method. By default, this method draws the whole source image, but by giving it more parameters it can be made to copy out a specific area. We used this for our game, by copying individual postures of the game character out of an image that contained many such postures.

To draw a shape in multiple orientations, transformations can be used. A 2d drawing context has a current transformation that can be changed with the translate, scale, and rotate methods. These will affect all subsequent drawing operations. A transformation state can be saved with the save method, and restored with the restore method.

When drawing an animation on a canvas, the clearRect method can be used to clear a part of the canvas, before redrawing it.

Exercises

Shapes

Write a program that draws the following shapes on a canvas:

  1. A parallelogram (a rectangle that is wider on one side).

  2. A red diamond (a rectangle rotated 45 degrees or ¼π radians).

  3. A zigzagging line.

  4. A spiral made up of 100 straight line segments.

  5. A yellow star.

The shapes to draw

When drawing the latter two, you may want to refer back to the explanation of Math.cos and Math.sin in Chapter 13, which describes how to get coordinates on a circle using these functions.

I recommend creating a function for each shape, and passing the position, and optionally other things, such as the size or the number of points, as parameters. The alternative, which is to hard-code numbers all over your code, tends to make the code needlessly hard to read and modify.

<canvas width="600" height="200"></canvas>
<script>
  var cx = document.querySelector("canvas").getContext("2d");

  // Your code here.
</script>

The parallelogram (1) is easy to draw using a path. Pick suitable center coordinates, and add each of the four corners around that.

The diamond (2) can be drawn the easy way, with a path, or the interesting way, with a rotate transformation. To use rotation, you will have to apply a trick similar to what we did in the flipHorizontally function. Because you want to rotate around the center of your rectangle, and not around the point (0,0), you must first translate to there, then rotate, and then translate back.

For the zigzag (3) it becomes unpractical to write a new call to lineTo for each line segment. Instead you should use a loop. You can either have each iteration draw two line segments (right and then left again), or one, in which case you must use the evenness (% 2) of the loop index to determine whether to go left or right.

You’ll also need a loop for the spiral (4). If you draw a series of points, with each point moving further along a circle around the spiral’s center, you get a circle. If, during the loop, you vary the radius of the circle on which you are putting the current point, and go around more than once, the result is a spiral.

The star (5) depicted is built out of quadraticCurveTo lines. You could also draw one with straight lines. Divide a circle into 8 (or however many points you want your star to have) pieces. Draw lines between these points, making them curve towards the center of the star (with quadraticCurveTo, you can use the center as control point).

The pie chart

Earlier in the chapter, we saw an example program that drew a pie chart. Modify this program so that the name of each category is shown next to the slice that represents it. Try to find a pleasing-looking way to automatically position this text, which would work for other data sets as well. You may assume that categories are no smaller than 5% (i.e. there won’t be a bunch of tiny ones next to each other).

You might again need Math.sin and Math.cos, as described in the previous exercise.

<canvas width="600" height="300"></canvas>
<script>
  var cx = document.querySelector("canvas").getContext("2d");
  var total = results.reduce(function(sum, choice) {
    return sum + choice.count;
  }, 0);

  var currentAngle = -0.5 * Math.PI;
  var centerX = 300, centerY = 150;
  // Add code to draw the slice labels in this loop.
  results.forEach(function(result) {
    var sliceAngle = (result.count / total) * 2 * Math.PI;
    cx.beginPath();
    cx.arc(centerX, centerY, 100,
           currentAngle, currentAngle + sliceAngle);
    currentAngle += sliceAngle;
    cx.lineTo(centerX, centerY);
    cx.fillStyle = result.color;
    cx.fill();
  });
</script>

You will need to call fillText, and set the context’s textAlign and textBaseline properties in such a way that the text ends up where you want it.

A sensible way to position the labels would be to put the text on the line going from the center of the pie through the middle of the slice. You don’t want to put the text directly against the side of the pie, but rather move it outside of it a given amount of pixels.

The angle of this line is currentAngle + 0.5 * sliceAngle. The code below finds a position on this line, 120 pixels from the center:

var middleAngle = currentAngle + 0.5 * sliceAngle;
var textX = Math.cos(middleAngle) * 120 + centerX;
var textY = Math.sin(middleAngle) * 120 + centerY;

For textBaseline, the value "middle" is probably appropriate when using this approach. What to use for textAlign depends on the side of the circle we are on—on the left, it should be "right", and on the right, it should be "left", so that the text is positioned away from the pie.

If you are not sure how to find out which side of the circle a given angle is on, look back to the explanation of Math.cos in the previous exercise. The cosine of an angle tells us which x coordinate it corresponds to, which in turn tells us exactly which side of the circle we are on.

A bouncing ball

Use the requestAnimationFrame technique that we saw in Chapter 13 and Chapter 15 to draw a box with a bouncing ball inside of it. The ball moves at a constant speed, and bounces off the box’s sides when it hits them.

<canvas width="400" height="400"></canvas>
<script>
  var cx = document.querySelector("canvas").getContext("2d");

  var lastTime = null;
  function frame(time) {
    if (lastTime != null)
      updateAnimation(Math.min(100, time - lastTime) / 1000);
    lastTime = time;
    requestAnimationFrame(frame);
  }
  requestAnimationFrame(frame);

  function updateAnimation(step) {
    // Your code here.
  }
</script>

A box is easy to draw with strokeRect. Define a variable that holds its size (or two variables, if your box’s width and height differ). To create a round ball, start a path, call arc(x, y, radius, 0, 7)—an arc going from zero to more than a whole circle—and fill it.

To model the ball’s position and speed, you can use the Vector type from Chapter 15(which is available on this page). Give it a starting speed, preferably one that is not purely vertical or horizontal, and every frame, multiply that speed with the amount of time that elapsed. When the ball gets too close to a vertical wall, invert the x component in its speed. Likewise, invert the y component when it hits a horizontal wall.

After finding the ball’s new position and speed, use clearRect to delete the scene, and redraw it using the new position.

Precomputed mirroring

One unfortunate thing about transformations is that they slow down drawing of bitmaps. For vector graphics, the effect is less serious, since there only a few points (for example the center of a circle) need to be transformed, after which drawing can happen as normal. For a bitmap image, the position of each pixel has to be transformed, and though it is possible that browsers will get more clever about this in the future, this currently causes a measurable increase in the time it takes to draw a bitmap.

In a game like ours, where we are only drawing a single transformed sprite, this is a non-issue. But imagine that we need to draw hundreds of characters, or thousands of rotating particles from an explosion.

Think of a way to allow us to draw an inverted character, without loading additional image files, and without having to make transformed drawImage calls every frame.

The key to the solution is the fact that we can use a canvas element as a source image when using drawImage. It is possible to create an extra <canvas> element, without adding it to the document, and draw our inverted sprites to it, once. When drawing an actual frame, we just copy the already inverted sprites to the main canvas.

Some care would be required because images do not load instantly. We only do the inverted drawing once, and if we do it before the image loads, it won’t draw anything. A "load" handler on the image can be used to draw the inverted images to the extra canvas. This canvas can be used as a drawing source immediately (it’ll simply be blank until we draw the character onto it).