Design Challenge Tutorial - Prototyping UI with Canvas

Download all the files as ZIP archive

What's Canvas, Precious?

Canvas is an HTML tag, part of the HTML 5 standard:

<canvas></canvas>

By iteslf, it does nothing. But Javascript running in the page can treat the Canvas element as a freeform drawing area, and dynamically fill it with whatever combination of graphics and text you want:

  var canvas = document.getElementById("the-canvas");
  var ctx = canvas.getContext("2d");

  ctx.fillStyle = "rgb(200,0,0)";
  ctx.fillRect (10, 10, 55, 50);

What can I do with Canvas?

Things that would previously require a plugin like Flash: freeform animation and interactivity (although not sound). You can write your animation for Canvas using Javascript, a language you probably already know if you're a web developer, instead of learning a new domain-specific plugin language. Users of your page can interact with it without needing to have any plugins installed, and since it's all part of the page they can see the source code simply by choosing "View Source".

Here are some examples of cool things you can do with Canvas:

One major drawback is that although Canvas works in Firefox, Opera, Safari, and Chrome, it is not yet supported by Internet Explorer. However, there are workarounds for this, such as Explorer Canvas.

I find Canvas to be a quick and fun way of creating prototypes for user interfaces. With Canvas, you're not limited to the buttons, fields, and menus of the typical HTML form or GUI toolkit -- instead, by defining the response to individual mouse-click and keystroke events, you can customize behavior down to a very low level, and create entirely novel styles of interaction.

A final advantage is that because Canvas simply runs in a page, making changes is very quick. No need to compile anything or to restart your browser: All you have to do is save a change to the Javascript source code and then reload the page.

Getting Started

Start with the demo.html and demo.js files. These are included in the ZIP download at the top of this page. Do "File"->"Open" in Firefox and pick demo.html to load the page from the file into a new tab.

You'll see a couple of overlapping squares. If you look at the demo.js source code, you'll see how these are drawn:

  function initCanvas() {
    var canvas = document.getElementById("the-canvas");
    var ctx = canvas.getContext("2d");

    ctx.fillStyle = "rgb(200,0,0)";
    ctx.fillRect (10, 10, 55, 50);

    ctx.fillStyle = "rgba(0, 0, 200, 0.5)";
    ctx.fillRect (30, 30, 55, 50);
  }

document.getElementById allows us to grab a reference to the canvas element itself. getContext("2d") gives us a context object; almost everything else we do to draw into the canvas will be done by calling various methods on this context object.

fillStyle and fillRect are methods of the context object that allow us to draw graphical primitives to arbitrary locations on the canvas. (The second rectangle shows off transparency, one of the advanced graphics features in Canvas -- the 0.5 argument is an alpha, or transparency, value.)

How does the initCanvas() function get called? The key is the following lines in demo.html:

  <script type="application/x-javascript" src="demo.js"></script>
  <body onload="initCanvas();">

The <script> tag imports the javascript file demo.js, where the initCanvas() function is defined. The onload attribute of the <body> tag defines a function that will be called as soon as the body of the page has finished loading. There are several other ways we could have achieved the same result, but this is one of the easiest, and is perfectly fine for the purpose of this demo.

A Rudimentary Photo Album App

We're going to use Canvas to build a very primitive version of a photo-album application, in which the user can scroll through a page where various images are displayed, and can freely reposition those images.

In each step of this tutorial, we'll make changes to demo.html and demo.js, then reload the tab to see the changes in action.

Displaying Images

Let's get rid of the code that draws colored squares and replace it with code to load an image from a file and display it in the canvas. (The image file we'll be using is included in the ZIP archive and is called lizard.JPG.) Replace the contents of demo.js with the following code (you can copy and paste it):
  function initCanvas() {
    drawImage();
  }

  var squareX = 100;
  var squareY = 100;

  function drawImage() {
    var canvas = document.getElementById("the-canvas");
    var ctx = canvas.getContext("2d");

    var imageFile = "lizard.JPG";
    var imageObject = new Image();
    imageObject.src = imageFile;
    imageObject.onload = function() {
      ctx.drawImage(imageObject, squareX, squareY, 100, 100);
    };
  }

Reload demo.html to see it in action. Pay attention to the line that says new Image();. This dynamically creates an image objct. This object is exactly the same thing as we could add to the page statically by putting an <img> tag into the HTML directly. Once it's created, we can set the same properties on it. If you've ever written a web page, you surely know about using <img src=""> to put an image in a page. In Javascript, we can set the value of the src attribute, meaning the URL of the image file, to anything we want, simply by assigning to imageObject.src. This works for other attributes of the image, too.

The drawImage() method of the context object is what actually draws the image; until then, it's in memory but not yet visible on the page. Because it can take a while for the image to load, however, calling drawImage() immediately sometimes results in nothing visible being displayed. We want to wait until the image is done loading before we draw it, so we do the same thing we did with the <body> tag of the HTML page -- we set the onload attribute to a function; this function gets called when the image is done loading. Setting up "callback" functions like this is an extremely common technique in Javascript, as is defining an anonymous function on the fly as we do here.

The arguments to the drawImage() method include the X and Y location of the upper-left corner of the image (measured in pixels from the upper-left corner of the canvas), followed by the width and height (in pixels) (optional). The lizard.JPG file is bigger than 100 by 100 pixels, so it will be scaled down to 100 by 100 for display.

So far we haven't done anything that we couldn't do with a static HTML file, but notice that the location of the image is coming from two variables, squareX and squareY. We'll take advantage of this when we add event handling.

Debugging

When working in any new development environment, one of the first things you'll need to know is how to figure out what's going wrong! I'm going to show you how, but first, you'll need to introduce an error. So add a random typo to your javascript and then save the file:

    var imageObject = new Imageasdf();

Now reload your page. Oh no! It doesn't work!

Go to "Tools" -> "Error Console". (On Mac, this is Apple-shift-J.) This brings up a window where you can see all the errors that happened in your page.

At the moment, the error console is probably full of pages and pages of errors from all different pages in all different tabs, plus extensions. We'll need to narrow that down a little to be able to see the error from your Canvas page. So hit the "Clear" button, then back in the main Firefox window, reload your Canvas page; then go back to the Error Console. At this point, you should see only one error: the one we care about.

It will have a filename line number on it, which will be helpful when debugging larger projects with many functions spread across multiple javascript files.

Handling Mouse Events

Assuming that you have fixed your error, let's move on to see how we can add a function to handle mouse events. Add this code to demo.js:

  function mouseDownHandler(evt) {
    var canvas = document.getElementById("the-canvas");
    squareX = evt.pageX - canvas.offsetLeft;
    squareY = evt.pageY - canvas.offsetTop;
    drawImage();
  }

And in demo.html, change the <body> tag to say this:

  <body onload="initCanvas();"
           onmousedown="mouseDownHandler(event);">

We're adding a new handler to the body tag, so that in addition to calling initCanvas() when it's done loading, it will call mouseDownHandler() whenver the user clicks the mouse button down within the page. mouseDownHandler() then sets the values of squareX and squareY to the location of the click, which can be obtained from the properties of the event object that was passed into mouseDownHandler(). (Note that pageX and pageY are relative to the edge of the page, but the location at which we draw the image is relative to the edge of the canvas, so we have to subtract the difference to get the image showing up in the right place.) Reload the page and try clicking around!

Note how a new image is getting displayed in each place where we click, but the old one is not going away? If that's not what we want, then we need to clear the canvas object to get rid of the old image before drawing the new one. Add a call to clearRect to the drawImage() function:

  function drawImage() {
    var canvas = document.getElementById("the-canvas");
    var ctx = canvas.getContext("2d");

    ctx.clearRect(0, 0, 600, 600);
    var imageFile = "lizard.JPG";
    // etc...

Try it again, and you'll see that now only one image appears at a time. This is the basis of all computer animation, by the way: Draw something to the screen, then erase it, then update its location, then redraw it at the new location.

As we'll see later, handlers for mouseup events and mousemove events can be attatched in the same way. By handling mouse down, mouse move, and mouse up events, you can write interfaces that respond to dragging actions on the part of the user.

Handling Keyboard Events

Handling keyboard events is not much different from handling mouse events. Add the following code into demo.js:

  var KEY = { RIGHT:39, UP:38, LEFT:37, DOWN:40 };

  function press(evt) {
    var code = evt.keyCode;
    switch(code) {
      case KEY.UP:
      squareY -= 10;
      drawImage();
      break;

      case KEY.DOWN:
      squareY += 10;
      drawImage();
      break;

      case KEY.LEFT:
      squareX -= 10;
      drawImage();
      break;

      case KEY.RIGHT:
      squareX +=10;
      drawImage();
      break;
    }
  }

And add a keydown handler to the <body> of demo.html:

  <body onload="initCanvas();"
       onmousedown="mouseDownHandler(event);"
       onkeydown="press(event);">

Just like the mouse handler function, the key handler function will "magically" get passed an event object. Since this is a keyboard event, it contains an attribute keyCode. We use the value of the keyCode to determine which key was hit. The code above responds to the four arrow keys by moving the image in an appropriate direction and then redrawing it.

Putting it all together

The files demo-complete.html and demo-complete.js (included in the ZIP archive) contain a version that puts together our mouse and keyboard handlers, along with an array of image-encapsulating objects, to implement a primitive multi-image viewer. Try it out! Load demo-complete.html in a tab; use the arrow keys to pan around, and the mouse to reposition images.

Acknowledgements and Further Reading

I drew most of the information in this presentation from the Mozilla canvas documentation on developer.mozilla.org, which is here: