Introduction to HTML5 Canvas with PacMan Action

HTML5 Canvas is undoubtedly one of the coolest features of HTML5 and is my personal favorite (followed closely by WebSockets). There are a number of interesting business applications but I find it most interesting because it is great for building games. Let’s dig into drawing and look into animation a bit to learn about Canvas. Since this is all client-side, we’ll make everything work right here in the browser. That way you don’t have to download the samples to see them run. All you need is a moderately recent browser.

We start with a basic canvas tag. Nothing fancy. We put a border around it so we can see it and set its width and height as a part of the tag. We do not set its dimensions through CSS since that will actually skew the dimensions of the drawing. A canvas tag would look something like this:


<canvas width="800" height="200" style="border: solid 1px #000;" id="canvas">
</canvas>

There isn’t actually much to canvas in HTML, even though it is bona fide HTML5. Almost everything about canvas is implemented through JavaScript. Let’s get started and see what that looks like.

Canvas and Context

To get access to the canvas object and start drawing, we need to write a little script to get it like so:


<script>
  window.onload = function () {
    var canvas = document.getElementById('canvas');
    var c = canvas.getContext('2d');
  }
</script>

This script gets us two things. First, it gets us access to the canvas element itself, which we will rarely use. Second, it gets us the 2d context of the canvas, which is what we will use most of the time when programming our canvas.

The name of the canvas fits well; the canvas is something you paint on, like a painter’s canvas. This metaphor works well for the element. The nomenclature of the context doesn’t fit well at all into the metaphor though. I find it is best to think of the context as your brush. How do you paint things on the canvas? Well, you use your brush! Let’s look at a simple example.


  (function () {
    window.onload = function () {
      var canvas = document.getElementById('canvas0');
      var c = canvas.getContext('2d');
      c.beginPath(); //To start drawing a line, you begin your path.
      c.moveTo(0, 0); //You have to start somewhere!
      c.lineTo(100, 50); //This is where you want to go. In other words, the second point.
      c.lineWidth = 2; //Sets the width of the line.
      c.strokeStyle = '#000'; //Sets the color of the line.
      c.stroke(); //Tells the context to draw the stroke.
    }
  })();

I wrapped all the JavaScript in a self-executing function so that the code doesn’st pollute the global scope. Then we get the canvas and its context, then start drawing. From c.beginPath() up until c.stroke(), all you are doing is defining a path. The stroke function tells the context, your brush, to draw the path. A path is like a collection of lines. To draw a simple line, you draw a path with one piece to it like we did above. Obviously you could keep going and send the line somewhere else with another c.lineTo(x, y) call.

One thing that you will immediately notice is that all canvas drawing involves writing code. Canvas is not a markup/declarative-style drawing system like some others (notably XAML for those of you who are Silverlight/WPF/Windows 8 fans). There may be projects out there to do this for canvas and if not you could certainly create one but the API for canvas is all code.

Let’s Animate!

So that was simple drawing. We should now do some simple animation. For animating things over time, you basically have two options, using setInterval (old school) or by using requestAnimationFrame. Let’s try both on for size. Since we want to create PacMan, we will leave our line behind and start using the canvas arc API. To create an animated circle, we could do something like this. Click on the canvas to start the animation (you will probably notice a problem).


var canvas, c;
window.addEventListener('load', function () {
  canvas = document.getElementById('canvas1');
  c = canvas.getContext('2d');
  //Whenever the canvas is clicked, update and draw the circle every 16 milliseconds.
  canvas.addEventListener('click', function () {
      setInterval(function () {
        pacman.x += 3;
        pacman.draw();
      }, 16)
    }, false
  );
}, false);
//Separating our PacMan object out. Better organized code FTW.
var pacman = {
  x: 100,
  draw: function () {
    //These arguments are...
    //  this.x - x position of the center of the circle
    //  100 - y position of the center of the circle
    //  50 - the radius of the circle
    //  (Math.PI / 180) * 0 - The angle to start drawing
    //  (Math.PI / 180) * 360 - The angle to stop drawing
    c.arc(this.x, 100, 50, (Math.PI / 180) * 0, (Math.PI / 180) * 360);
    c.fillStyle = '#FF0';
    c.fill();
  }
};

Two important things to discuss here. First, you do not draw circles on the canvas, you draw arcs. If you want a circle, you draw an arc to make one. The length of an arc is defined in radians and a circle has 2pie radians, so about 6.283. If you are like me, you are more used to thinking in angles, so a simple equation to use to convert radians to 0-360 angles is the following: (Math.PI / 180) * angle. That’s why you see that in the code sample above.

If you ran the sample above (by clicking on it), you probably noticed that instead of a circle going across the screen you saw an elongating yellow shape. If you go back to the painting canvas metaphor, this actually makes a lot of sense. If you paint a circle on a canvas and then another, the first one does not disappear. To make it go away, you have to paint over it or remove the paint. The HTML5 canvas works exactly the same way. To get our circle going across the screen properly, we have to clear the canvas before we draw again. The easiest way to do that is to add the following right above where the draw() method of PacMan is called.

  c.clearRect(0, 0, canvas.width, canvas.height);

var canvas, c;
window.addEventListener('load', function () {
  canvas = document.getElementById('canvas2');
  c = canvas.getContext('2d');
  canvas.addEventListener('click', function () {
    setInterval(function () {
      pacman.x += 3;
      c.clearRect(0, 0, canvas.width, canvas.height);
      pacman.draw();
    }, 16)
  }, false
  );
}, false);
var pacman = {
  x: 100,
  draw: function () {
    c.beginPath();
    c.arc(this.x, 100, 50, (Math.PI / 180) * 0, (Math.PI / 180) * 360);
    c.fillStyle = '#FF0';
    c.fill();
  }
};

There is a better way to control your canvas painting than setInterval and that is to use requestAnimationFrame. This API is better for animation because, unlike setInterval, this will get fired when the browser thinks it is ready to draw (up to 60 frames per second), not when your setTimout thinks it is time to draw, so it is a more performance friendly approach. And related to that, requestAnimationFrame will sometimes stop being called at all when the browser (or browser tab) is no longer in the foreground (in my testing Chrome puts it to sleep entirely), so it doesn’t chew up your CPU when it is not necessary.

Of course the negative to this approach is that it isn’t as well supported as we would hope. It is in current versions of Firefox, Chrome and Internet Explorer but is not in Internet Explorer 9, so a polyfill would be helpful. Fortunately, Paul Irish has already done this and I can just borrow his. I will now update our script from before with this new polyfill and we’ll see what we have.


//This is our polyfill.

window.requestAnimationFrame = (function () {
  return window.requestAnimationFrame ||
          window.webkitRequestAnimationFrame ||
          window.mozRequestAnimationFrame ||
          window.oRequestAnimationFrame ||
          window.msRequestAnimationFrame ||
          function (callback) {
            window.setTimeout(callback, 1000 / 60);
          };
})();

var canvas, c;
window.addEventListener('load', function () {
  canvas = document.getElementById('canvas3');
  c = canvas.getContext('2d');
  canvas.addEventListener('click', function () {
      window.requestAnimationFrame(drawLoop);
    }, false
  );
}, false);

function drawLoop() {
  pacman.x += 5; //It was going a bit slow. I sped it up.
  c.clearRect(0, 0, canvas.width, canvas.height);
  pacman.draw();
  window.requestAnimationFrame(drawLoop);
}
var pacman = {
  x: 100,
  draw: function () {
    c.beginPath();
    c.arc(this.x, 100, 50, (Math.PI / 180) * 0, (Math.PI / 180) * 360);
    c.fillStyle = '#FF0';
    c.fill();
  }
};

There are several things to note about these changes. First, the draw loop needs to be callable by requestAnimationFrame so it was a little helpful to change it from being an anonymous function to a declared one. Second, note that drawLoop() calls requestAnimationFrame passing in itself. This is necessary for the animation to work. Third, note that the polyfill at the top ultimately downgrades to a setInterval for browsers that have no implementation of the feature.

PacMan

We have a basic animation loop in place so let’s focus on PacMan. Drawing his mouth is fairly easy to do. First, instead of drawing all the way around the circle, we want to start drawing a bit down from the start and stop a bit before we reach the end. Second, when we get to the end, we should take the end of the path and take it to the middle of the circle. It would look like this:


var pacman = {
  x: 100,
  draw: function () {
    c.beginPath();
    c.arc(this.x, 100, 50, (Math.PI / 180) * 40, (Math.PI / 180) * 320);
    c.lineTo(this.x, 100);
    c.fillStyle = '#FF0';
    c.fill();
  }
};

This might have worked a little better than you expected. The arc defines the circular curve around PacMan’s body, and the line completes the upper lip, but what completes the lower lip? You get this effect because of the nature of how fill() works. You see, when you call fill on a path that isn’t closed like this one, it has to extrapolate between the beginning and ending of the path to calculate how to fill. In other words, we don’t have to figure out the math in this case to finish off the path. Canvas does that for us.

Next we should animate his mouth. Currently the open position starts the arc 40 degrees after the starting point and ends 40 degrees before. To close the mouth we simply need to decrease that number over time and to open it, increase it again. Simple. Here is an implementation that does that.


var pacman = {
  x: 100,
  mouthOpenValue: 40,
  mouthPosition: -1,
  
  draw: function () {
    if (this.mouthOpenValue <= 0)
      this.mouthPosition = 1;
    else if (this.mouthOpenValue >= 40)
      this.mouthPosition = -1;
    this.mouthOpenValue += (5 * this.mouthPosition);
    c.beginPath();
    c.arc(this.x, 100, 50, 
      (Math.PI / 180) * this.mouthOpenValue,
      (Math.PI / 180) * (360 - this.mouthOpenValue));
    c.lineTo(this.x, 100);
    c.fillStyle = '#FF0';
    c.fill();
  }
};

Let’s talk about mouthOpenValue a bit. For some of you, this may be obvious. But for those that find this strange, here is the thinking. This variable is there to keep track of whether his mouth should be opening or closing. Someone might be tempted to use string values like 'open' or 'closed' instead of the 1/-1 that I am using. On the one hand, that does make some sense. Using something like 'open'/'closed' says more about the actual function of the variable. On the other hand, by using a positive or negative 1, we save a little code. If it is a string instead of the number as I did it, you have to have a separate bit of code to specify whether or not the degree angle should be moving up for down. In other words, you can’t just multiply the amount of movement by mouthOpenValue to use with the arc code. Either way is fine. How I implemented it just makes the code a little more terse. I hope that makes since because I am going to do that again in just a bit.

Turning Around

Now that we have animated our mouth, we need to turn PacMan around when we get to the end of the canvas. This means we will need to do two things, start decreasing his x position when he gets to the far right and draw the mouth on the other side when he is heading left. We will continue to take things slowly and do this one step at a time. First we will work on him moving left.

To implement direction, I do something very similar to what I did with mouthPosition. I create another variable on the PacMan object to designate what direction he should be traveling. I then use that to calculate how he moves left or right. It looks like this:


var pacman = {
  x: 100,
  mouthOpenValue: 40,
  mouthPosition: -1,
  direction: 1,
  draw: function () {
    if (this.mouthOpenValue <= 0)
      this.mouthPosition = 1;
    else if (this.mouthOpenValue >= 40)
      this.mouthPosition = -1;
  
    if (this.x >= canvas.width - 50)
      this.direction = -1;
    else if (this.x <= 50)
      this.direction = 1;
  
    this.x += (7 * this.direction);
    this.mouthOpenValue += (5 * this.mouthPosition);
    c.beginPath();
    c.arc(this.x, 100, 50, (Math.PI / 180) * this.mouthOpenValue, (Math.PI / 180) * (360 - this.mouthOpenValue));
    c.lineTo(this.x, 100);
    c.fillStyle = '#FF0';
    c.fill();
  }
};

We are now very close to being finished. We simply need to put the mouth on the other side of PacMan when he is heading left. If heading right, the beginning of the circle starts at 0 and ends at 360. If heading left, we need to animate based on the other side of the circle, i.e., 180. So in theory, if heading left we just used 180 degrees as our starting point for mouth animation, we would be just fine.


var pacman = {
  x: 100,
  mouthOpenValue: 40,
  mouthPosition: -1,
  direction: 1,
  draw: function () {
    if (this.mouthOpenValue <= 0)
      this.mouthPosition = 1;
    else if (this.mouthOpenValue >= 40)
      this.mouthPosition = -1;
    
    if (this.x >= canvas.width - 50)
      this.direction = -1;
    else if (this.x <= 50)
      this.direction = 1;
    
    this.x += (7 * this.direction);
    this.mouthOpenValue += (5 * this.mouthPosition);
    c.beginPath();
    if (this.direction === 1) {
      c.arc(this.x, 100, 50, (Math.PI / 180) * this.mouthOpenValue, (Math.PI / 180) * (360 - this.mouthOpenValue));
    }
    else {
      c.arc(this.x, 100, 50, (Math.PI / 180) * (180 - this.mouthOpenValue), (Math.PI / 180) * (180 + this.mouthOpenValue), true);
    }
    c.lineTo(this.x, 100);
    c.fillStyle = '#FF0';
    c.fill();
  }
};

We are so close now but most will notice when they run the animation that there is a slight flicker with the animation that happens with PacMan is moving left. Why is that? This happens when the variable mouthOpenValue is equal to zero. When that is the case, the circle is drawn from 180 degrees to 180 degrees and it draws nothing. This is unfortunate but easy to fix. There are transformations that we could apply to the canvas to implement this very differently or we could take the easy route and make sure one of the values when heading left is 181 instead of 180. Here is the final, full code listing with that last change.


window.requestAnimationFrame = (function () {
  return window.requestAnimationFrame ||
          window.webkitRequestAnimationFrame ||
          window.mozRequestAnimationFrame ||
          window.oRequestAnimationFrame ||
          window.msRequestAnimationFrame ||
          function (callback) {
            window.setTimeout(callback, 1000 / 60);
          };
})();
var canvas, c;
window.addEventListener('load', function () {
  canvas = document.getElementById('canvas8');
  c = canvas.getContext('2d');
  canvas.addEventListener('click', function () {
    window.requestAnimationFrame(drawLoop);
  }, false
  );
}, false);
function drawLoop() {
  c.clearRect(0, 0, canvas.width, canvas.height);
  pacman.draw();
  window.requestAnimationFrame(drawLoop);
}
var pacman = {
  x: 100,
  mouthOpenValue: 40,
  mouthPosition: -1,
  direction: 1,
  draw: function () {
    if (this.mouthOpenValue <= 0)
      this.mouthPosition = 1;
    else if (this.mouthOpenValue >= 40)
      this.mouthPosition = -1;
    if (this.x >= canvas.width - 50)
      this.direction = -1;
    else if (this.x <= 50)
      this.direction = 1;
    this.x += (7 * this.direction);
    this.mouthOpenValue += (5 * this.mouthPosition);
    c.beginPath();
    if (this.direction === 1) {
      c.arc(this.x, 100, 50, (Math.PI / 180) * this.mouthOpenValue, (Math.PI / 180) * (360 - this.mouthOpenValue));
    }
    else {
      c.arc(this.x, 100, 50, (Math.PI / 180) * (179 - this.mouthOpenValue), (Math.PI / 180) * (180 + this.mouthOpenValue), true);
    }
    c.lineTo(this.x, 100);
    c.fillStyle = '#FF0';
    c.fill();
  }
};

So that is a short introduction to drawing and animating with canvas. I hope you found it useful. If you have questions, please leave them in the comments or shoot me an email. Enjoy!

comments powered by Disqus