Having fun with HTML5 — Canvas, part 2

Ear­li­er I explored some of the basic draw­ing meth­ods avail­able on the 2D con­text of the new can­vas ele­ment in HTML5, mov­ing on from there, I’ve put togeth­er anoth­er quick demo here (see image below) which lets the user scrib­ble inside the can­vas ele­ment.

image

HTML

The HTML for the page is sim­ple enough, the key thing is obvi­ous­ly the can­vas ele­ment:

<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 can­vas ele­ment is as below:

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

I want to use the ‘crosshair’ cur­sor when­ev­er you’re in the can­vas area, but the above css only works when the left mouse but­ton is not held down (when by default, the cur­sor changes to the ‘text’ cur­sor) so in addi­tion to the css I added an event han­dler to change the cur­sor 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

Can­vas sup­port detec­tion

When the page fin­ish­es load­ing I first check if the brows­er sup­ports the can­vas ele­ment, again using the Mod­ern­izr library, and dis­play a warn­ing mes­sage if can­vas is not sup­port­ed:

$(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();
    }
});

Design­ing the inter­ac­tions

The inter­ac­tion I’m after here is such that you start draw­ing by click­ing and hold­ing down the left mouse but­ton and the path is drawn as you move the mouse while still hold­ing the mouse but­ton, and you stop draw­ing by releas­ing the mouse but­ton or mov­ing the mouse out of the can­vas area. So ulti­mate­ly it all boils down to the stan­dard onmouse­down, onmouse­move, onmouse­up and onmouse­out events.

There’s one more prob­lem which we have to solve first though – how to resolve the x, y coor­di­nates of the click in rela­tion to the can­vas (rather than the page itself). The prob­lem is that mouse events are imple­ment­ed dif­fer­ent­ly in dif­fer­ent browsers, so here’s a helper func­tion which encap­su­lates that par­tic­u­lar piece of log­ic:

// 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 han­dlers

First, we need to obtain ref­er­ences to both the can­vas ele­ment and the 2D draw­ing con­text so we can use them lat­er:

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 coor­di­nates of the click inside the can­vas 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 tak­en here is to attach the event han­dlers for mouse move/up/out inside the mouse down event han­dler (which marks the start of the draw­ing process), these han­dlers are then unbound when mouse up or mouse out event occurs (which marks the end of the draw­ing process).

This approach removes the need for a glob­al 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 log­i­cal start and end of the ‘draw­ing’ event (which as stat­ed before, starts at mouse down and fin­ish­es at either mouse up or mouse out) match­es the life­time of the han­dlers required to han­dle this so that we don’t han­dle the mouse move/up/out events unless we need to.

To round things off, there are two more han­dlers need­ed to clear the can­vas 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 sim­ple opti­miza­tion step to reduce the num­ber of times jQuery needs to look through the DOM to iden­ti­fy match­ing ele­ments. So that’s it, a sim­ple HTML5 page to let you scrib­ble in a can­vas page :-)

Related posts:

Hav­ing fun with HTML5 — Can­vas, part 1

Hav­ing fun with HTML5 — Can­vas, part 3

Hav­ing fun with HTML5 — Can­vas, part 4

Hav­ing fun with HTML5 — Can­vas, part 5