Canvas painting

Author:

Table of contents

Introduction

My previous canvas primer article provided you with insight into the numerous use cases for canvas in web applications. In this article we will explore how you can write your own canvas-based painting application.

Making a web application that allows users to draw on a canvas requires several important steps: setting up your HTML document with a canvas context (a canvas element with an id), setting up your script to target that canvas context and draw inside it and adding the required mouse event handlers for user interaction and associated logic. Once the event handlers are in place, it's then fairly simple to add any desired functionality.

The final painting application example looks like this:

The final painting application example from this article

To make it easier to follow along with the code walkthrough presented below, download the full code example and follow along with it as you read the article.

Getting started with the HTML

We shall begin with a minimal HTML document:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Paint</title>
    <style type="text/css"><!--
      #container { position: relative; }
      #imageView { border: 1px solid #000; }
    --></style>
  </head>
  <body>
    <div id="container">
      <canvas id="imageView" width="400" height="300">
        <p>Unfortunately, your browser is currently unsupported by our web 
        application.  We are sorry for the inconvenience. Please use one of the 
        supported browsers listed below, or draw the image you want using an 
        offline tool.</p>
        <p>Supported browsers: <a href="http://www.opera.com">Opera</a>, <a 
          href="http://www.mozilla.com">Firefox</a>, <a 
          href="http://www.apple.com/safari">Safari</a>, and <a 
          href="http://www.konqueror.org">Konqueror</a>.</p>
      </canvas>
    </div>

    <script type="text/javascript" 
    src="example1.js"></script>
  </body>
</html>

As you can see, we only have the bare bones of an HTML document here, with a canvas element contained inside. If the browser does not support Canvas, then the fallback content will show. We will add more markup later on, but this is all we need for now.

The fallback content you provide should be as helpful as possible. You should not just say something like "this web application is unsupported by your browser" - that would be basically useless. Tell the user what he/she can do to get your application to work (eg use a different web browser), or provide alternative solutions, like a file upload input which allows the user to upload a painting created offline. Naturally, the fallback content depends on the context where you have the painting application.

Canvas interaction

Now we have the canvas element in place, the next step is to make the element somehow interact with the mouse. We shall first test our interaction, and then go on to start adding in the functions we want our application to perform.

Testing the canvas interaction

For testing purposes we shall first try to paint something under the mouse. We can do that by attaching a mousemove event handler to the canvas element. Here's the gist of the example script:

// ...
function init () {
  // ...
  // Attach the mousemove event handler.
  canvas.addEventListener('mousemove', ev_mousemove, false);
}

// The mousemove event handler.
var started = false;
function ev_mousemove (ev) {
  var x, y;

  // Get the mouse position relative to the canvas element.
  if (ev.layerX || ev.layerX == 0) { // Firefox
    x = ev.layerX;
    y = ev.layerY;
  } else if (ev.offsetX || ev.offsetX == 0) { // Opera
    x = ev.offsetX;
    y = ev.offsetY;
  }

  // The event handler works like a drawing pencil which tracks the mouse 
  // movements. We start drawing a path made up of lines.
  if (!started) {
    context.beginPath();
    context.moveTo(x, y);
    started = true;
  } else {
    context.lineTo(x, y);
    context.stroke();
  }
}
// ...

Try the example live.

This code turned out to be a success: we are just starting to see how dynamic and cool canvas can be. We use the event.layer* / offset* properties to determine the mouse position relative to the canvas element. That's all we need to start drawing.

Implementing events

Let's take this script one step further. It's best to have a single event handler that only determines the coordinates relative to the canvas element. The implementation of each drawing tool should be split into independent functions. Lastly, drawing tools need to interact with the user for events like mousedown and mouseup as well, not just when moving the mouse (mousemove). Therefore, multiple event listeners will be added to the script.

The updated script including events contains the following snippet:

// ...
function init () {
  // ...
  // The pencil tool instance.
  tool = new tool_pencil();

  // Attach the mousedown, mousemove and mouseup event listeners.
  canvas.addEventListener('mousedown', ev_canvas, false);
  canvas.addEventListener('mousemove', ev_canvas, false);
  canvas.addEventListener('mouseup',   ev_canvas, false);
}

// This painting tool works like a drawing pencil which tracks the mouse 
// movements.
function tool_pencil () {
  var tool = this;
  this.started = false;

  // This is called when you start holding down the mouse button.
  // This starts the pencil drawing.
  this.mousedown = function (ev) {
      context.beginPath();
      context.moveTo(ev._x, ev._y);
      tool.started = true;
  };

  // This function is called every time you move the mouse. Obviously, it only 
  // draws if the tool.started state is set to true (when you are holding down 
  // the mouse button).
  this.mousemove = function (ev) {
    if (tool.started) {
      context.lineTo(ev._x, ev._y);
      context.stroke();
    }
  };

  // This is called when you release the mouse button.
  this.mouseup = function (ev) {
    if (tool.started) {
      tool.mousemove(ev);
      context.closePath();
      tool.started = false;
    }
  };
}

// The general-purpose event handler. This function just determines the mouse 
// position relative to the canvas element.
function ev_canvas (ev) {
  if (ev.layerX || ev.layerX == 0) { // Firefox
    ev._x = ev.layerX;
    ev._y = ev.layerY;
  } else if (ev.offsetX || ev.offsetX == 0) { // Opera
    ev._x = ev.offsetX;
    ev._y = ev.offsetY;
  }

  // Call the event handler of the tool.
  var func = tool[ev.type];
  if (func) {
    func(ev);
  }
}
// ...

Try the updated events example.

The script has been split into multiple functions. Now the canvas has three event listeners (mousedown, mousemove and mouseup). The ev_canvas() function adds two new properties to the DOM event object, _x and _y, which simply hold the mouse coordinates relative to the canvas. This event handler acts like a "proxy" by calling other functions, depending on the event type. If the event is mousemove, then tool.mousemove() is called, and so on. The event handlers associated with the active tool can use the properties added to the DOM event object.

With the above changes made, we are ready to kick things off. The drawing pencil works fine now, with the added start and end functions. All the pencil-related functions are grouped together in a single function object. Currently we only have the tool_pencil object present, instanced as tool, but we can easily add more objects.

Adding drawing tools

Let's add some more drawing tools, by adding more tool objects. Each new tool needs to implement some of the available events.

First, a drop-down menu will be added, to allow the user to select the different drawing tools. This is achieved by adding the following into the HTML document:

<body>
<p><label>Drawing tool: <select id="dtool">
  <option value="rect">Rectangle</option>
  <option value="pencil">Pencil</option>
</select></label></p>
<!-- ... -->
</body>

Then we update the script to handle more than just one tool:

// ...
// The active tool instance.
var tool = false;
var tool_default = 'rect';

function init () {
  // ...
  // Get the tool select input.
  var tool_select = document.getElementById('dtool');
  if (!tool_select) {
    alert('Error: failed to get the dtool element!');
    return;
  }
  tool_select.addEventListener('change', ev_tool_change, false);

  // Activate the default tool.
  if (tools[tool_default]) {
    tool = new tools[tool_default]();
    tool_select.value = tool_default;
  }
  // ...
}

// ...
// The event handler for any changes made to the tool selector.
function ev_tool_change (ev) {
  if (tools[this.value]) {
    tool = new tools[this.value]();
  }
}

// This object holds the implementation of each drawing tool.
var tools = {};

// The drawing pencil.
tools.pencil = function () {
  // ...
};

// ...

That should be enough. The code above just sets up an event handler for the <select> element. The implementation of each drawing tool is now inside a single tools object, and the tool variable just holds an instance of the active tool. The ev_tool_change() function makes sure that the tool variable is always an object instance of the tool picked by the user.

The benefit of the above code is that any tool can have its own instance logic, dependant on any factors you see fit. You can do anything you want when the tool is activated, for example ask the user for a string, number or some other input.

Now we've set up a solid groundwork for the tools and looked at the pencil implementation, let's now look at implementing some of the other individual tools.

Rectangle

You are now in for a surprise. Let's implement the rectangle tool and then test the code. Here's the updated script:

// ...
tools.rect = function () {
  var tool = this;
  this.started = false;

  this.mousedown = function (ev) {
    tool.started = true;
    tool.x0 = ev._x;
    tool.y0 = ev._y;
  };

  this.mousemove = function (ev) {
    if (!tool.started) {
      return;
    }

    var x = Math.min(ev._x,  tool.x0),
        y = Math.min(ev._y,  tool.y0),
        w = Math.abs(ev._x - tool.x0),
        h = Math.abs(ev._y - tool.y0);

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

    if (!w || !h) {
      return;
    }

    context.strokeRect(x, y, w, h);
  };

  this.mouseup = function (ev) {
    if (tool.started) {
      tool.mousemove(ev);
      tool.started = false;
    }
  };
};
// ...

The implementation of the new rectangle tool should be straight-forward and thus easy to understand. It maintains the same basic structure as the pencil tool. The difference is that for the rectangle we store the start point, which is needed so we can then draw the rectangle for each mouse move (live feedback).

Now, for the surprise: access the above example link and try drawing two rectangles. Notice any problem? Yes, that's right: the previous drawing is always lost because of the clearRect() method call. We cannot remove this call, because the tool becomes useless if we do so (every rectangle resize remains on screen, before you even release the mouse button to make your selection).

The solution is to use a temporary canvas for live feedback operations. The script initialization adds a new canvas element with the same dimensions as the original one, positioned on top. All tools must draw on the temporary canvas. When their drawing operation ends, the pixels they generated are then moved onto the background canvas.

Try the updated rectangle example - the rectangle tool now works fine.

Here are the script changes required. The initialization function now looks like so:

// ...
var canvas, context, canvaso, contexto;

function init () {
  // Find the canvas element.
  canvaso = document.getElementById('imageView');
  if (!canvaso) {
    alert('Error: I cannot find the canvas element!');
    return;
  }

  if (!canvaso.getContext) {
    alert('Error: no canvas.getContext!');
    return;
  }

  // Get the 2D canvas context.
  contexto = canvaso.getContext('2d');
  if (!contexto) {
    alert('Error: failed to getContext!');
    return;
  }

  // Add the temporary canvas.
  var container = canvaso.parentNode;
  canvas = document.createElement('canvas');
  if (!canvas) {
    alert('Error: I cannot create a new canvas element!');
    return;
  }

  canvas.id     = 'imageTemp';
  canvas.width  = canvaso.width;
  canvas.height = canvaso.height;
  container.appendChild(canvas);

  context = canvas.getContext('2d');

  // ...
}

The new img_update() function is as follows:

// This function draws the #imageTemp canvas on top of #imageView,
// after which #imageTemp is cleared. This function is called each time when the
// user completes a drawing operation.
function img_update () {
  contexto.drawImage(canvas, 0, 0);
  context.clearRect(0, 0, canvas.width, canvas.height);
}

When any drawing operation is complete, the img_update() function must be invoked, so that the new pixels get stored in the image. For example, here is the minor update for the pencil tool:

// The drawing pencil.
tools.pencil = function () {
  // ...
  this.mouseup = function (ev) {
    if (tool.started) {
      tool.mousemove(ev);
      context.closePath();
      tool.started = false;
      img_update();
    }
  };
};

In the case of the rectangle and pencil tools, the drawing operation is complete once the user releases the mouse button, so we simply add the call to img_update() into the mouseup event handler. However, note that this call is dependant on each drawing tool implementation, in order to ensure the additional flexibility required by other use cases.

The last part needing a minor update is the HTML document's CSS:

<style type="text/css">
  #container { position: relative; }
  #imageView { border: 1px solid #000; }
  #imageTemp { position: absolute; top: 1px; left: 1px; }
</style>

The CSS above rules are needed to properly position the temporary <canvas> element on top of the original one.

Line

With everything in place, adding new tools becomes easier and easier. The JavaScript implementation including the line tool looks like this:

tools.line = function () {
  var tool = this;
  this.started = false;

  this.mousedown = function (ev) {
    tool.started = true;
    tool.x0 = ev._x;
    tool.y0 = ev._y;
  };

  this.mousemove = function (ev) {
    if (!tool.started) {
      return;
    }

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

    context.beginPath();
    context.moveTo(tool.x0, tool.y0);
    context.lineTo(ev._x,   ev._y);
    context.stroke();
    context.closePath();
  };

  this.mouseup = function (ev) {
    if (tool.started) {
      tool.mousemove(ev);
      tool.started = false;
      img_update();
    }
  };
};

That's it! Try the line tool example for yourself.

The line tool is very similar to the rectangle tool. The mousedown() function stores the starting point, which is then used in mousemove() for drawing the actual line.

What's next?

The above should give you a fairly good understanding of what it takes to start developing an online paint application. Besides just drawing on the canvas you need to take into consideration other aspects as well, such as:

If you want to learn more, you can take a look at the open-source project Paint.Web. This tutorial is based on the code used for Paint.Web, thus you should already have a head-start in understanding that code. At the moment, Paint.Web is in a permanent state of evolution, and it has already tackled some of the aspects mentioned above.