Author: Mihai Şucan (ROBO Design)
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:
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.
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.
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.
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();
}
}
// ...
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.
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.
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.
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.
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.
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.