Having fun with HTML5 – Canvas, part 2

Earlier I explored some of the basic drawing methods available on the 2D context of the new canvas element in HTML5, moving on from there, I’ve put together another quick demo here (see image below) which lets the user scribble inside the canvas element.

image

HTML

The HTML for the page is simple enough, the key thing is obviously the canvas element:

<section id="wrapper">
    <h1>HTML5 Demo</h1>
    <aside id="message"></aside>

    <article>
        <section class="block">
            <h4>Drawing tools:</h4>
            <article>
                Line width:
                <input type="text" id="btnLinewidth" value="1"></input> pixels
            </article>
            <article>
                <input type="submit" id="btnClear" value="Clear"></input>
            </article>
        </section>
        <section class="block">
            <h4>Canvas area:</h4>
            <canvas id="drawingCanvas" width="400" height="400" class="block"
                    onSelectStart="this.style.cursor='crosshair'; return false;"></canvas>
        </section>
    </article>
</section>

CSS

The css for the canvas element is as below:

#drawingCanvas
{
    border-style: dotted;
    border-width: 1px;
    border-color: Black;
    cursor: crosshair;
}

I want to use the ‘crosshair’ cursor whenever you’re in the canvas area, but the above css only works when the left mouse button is not held down (when by default, the cursor changes to the ‘text’ cursor) so in addition to the css I added an event handler to change the cursor on the select start event in the HTML mark up:

<canvas id="drawingCanvas" width="400" height="400" class="block"
        onSelectStart="this.style.cursor='crosshair'; return false;"></canvas>

Javascript

Canvas support detection

When the page finishes loading I first check if the browser supports the canvas element, again using the Modernizr library, and display a warning message if canvas is not supported:

$(document).ready(function () {
    // display a warning message if canvas is not supported
    if (!Modernizr.canvas) {
        $("#message").html("<p><b>WARNING</b>: Your browser does not support HTML5's canvas feature, you won't be able to see the drawings below</p>");
        $("article").hide();
    } else {
        initialize();
    }
});

Designing the interactions

The interaction I’m after here is such that you start drawing by clicking and holding down the left mouse button and the path is drawn as you move the mouse while still holding the mouse button, and you stop drawing by releasing the mouse button or moving the mouse out of the canvas area. So ultimately it all boils down to the standard onmousedown, onmousemove, onmouseup and onmouseout events.

There’s one more problem which we have to solve first though – how to resolve the x, y coordinates of the click in relation to the canvas (rather than the page itself). The problem is that mouse events are implemented differently in different browsers, so here’s a helper function which encapsulates that particular piece of logic:

// works out the X, Y coordinates of the click INSIDE the canvas element from the X, Y
// coordinates on the page
function getPosition(mouseEvent, element) {
    var x, y;
    if (mouseEvent.pageX != undefined && mouseEvent.pageY != undefined) {
        x = mouseEvent.pageX;
        y = mouseEvent.pageY;
    } else {
        x = mouseEvent.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
        y = mouseEvent.clientY + document.body.scrollTop + document.documentElement.scrollTop;
    }
    return { X: x - element.offsetLeft, Y: y - element.offsetTop };
}

Mouse event handlers

First, we need to obtain references to both the canvas element and the 2D drawing context so we can use them later:

var element = document.getElementById("drawingCanvas");
var context = element.getContext("2d");

For the mouse down event, we need to first work out the x and y coordinates of the click inside the canvas then start our path at that point:

// start drawing when the mousedown event fires, and attach handlers to
// draw a line to wherever the mouse moves to
$("#drawingCanvas").mousedown(function (mouseEvent) {
    var position = getPosition(mouseEvent, element);
    context.moveTo(position.X, position.Y);
    context.beginPath();

    // attach event handlers
    $(this).mousemove(function (mouseEvent) {
        drawLine(mouseEvent, element, context);
    }).mouseup(function (mouseEvent) {
        finishDrawing(mouseEvent, element, context);
    }).mouseout(function (mouseEvent) {
        finishDrawing(mouseEvent, element, context);
    });
});

// draws a line to the x and y coordinates of the mouse event inside
// the specified element using the specified context
function drawLine(mouseEvent, element, context) {
    var position = getPosition(event, element);
    context.lineTo(position.X, position.Y);
    context.stroke();
}

// draws a line from the last coordiantes in the path to the finishing
// coordinates and unbind any event handlers which need to be preceded
// by the mouse down event
function finishDrawing(mouseEvent, element, context) {
    // draw the line to the finishing coordinates
    drawLine(mouseEvent, element, context);

    context.closePath();

    // unbind any events which could draw
    $(element).unbind("mousemove").unbind("mouseup").unbind("mouseout");
}

The approach I have taken here is to attach the event handlers for mouse move/up/out inside the mouse down event handler (which marks the start of the drawing process), these handlers are then unbound when mouse up or mouse out event occurs (which marks the end of the drawing process).

This approach removes the need for a global flag (don’t you just hate those by now!?) to track when the code needs to draw a line. It also makes sure that the logical start and end of the ‘drawing’ event (which as stated before, starts at mouse down and finishes at either mouse up or mouse out) matches the lifetime of the handlers required to handle this so that we don’t handle the mouse move/up/out events unless we need to.

To round things off, there are two more handlers needed to clear the canvas and to change the width of the lines being drawn:

// clear the content of the canvas by resizing the element
$("#btnClear").click(function () {
    // remember the current line width
    var currentWidth = context.lineWidth;

    element.width = element.width;
    context.lineWidth = currentWidth;
});

// change the line width
$("#btnLinewidth").change(function (event) {
    if (!isNaN(event.target.value)) {
        context.lineWidth = event.target.value;
    }
});

Oh and one more thing, you might have noticed that I’ve chained some of the method calls, such as this:

$(element).unbind("mousemove").unbind("mouseup").unbind("mouseout");

This is a simple optimization step to reduce the number of times jQuery needs to look through the DOM to identify matching elements. So that’s it, a simple HTML5 page to let you scribble in a canvas page :-)

Related posts:

Having fun with HTML5 – Canvas, part 1

Having fun with HTML5 – Canvas, part 3

Having fun with HTML5 – Canvas, part 4

Having fun with HTML5 – Canvas, part 5