HTML5 Canvas Sketch

I’ve been (slowly) working on an app for kids recently, and it’s based around a drawing construct. While my first thought was ‘this is going to be hard,’ it turned out that it’s a pretty straightforward space to work in. On day zero I found this dev.opera page about working with a canvas and drawing.

To be fair, that seemed to be the whole goal in a nutshell…did what I wanted, behaved logically, so I could build off of it, etc. But then someone working on a different drawing app (it’s live: Dumpling) reached out for some guidance, and together we worked through a variety of sources (this was awhile ago now, but I’ll add citation as I recall them) and streamlined it (from both a code, and graphical perspective).

The basic idea is that you take an HTML5 canvas element, and attach some event listeners for when the mouse button is pressed, the mouse is moved, the button is released…

So here was the start of my HTML:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Draw</title>
<style type="text/css">
body {background:#ccc; margin:0; padding:0}
html {margin:0; padding:0;}
#container { position: relative; margin:0; padding:0px; }
#canvas { border: 1px solid #000; position:absolute; width:298px; margin-left:11px; margin-top:5px; z-index:20; }
#under { border: 1px solid #000; position:absolute; width:298px; margin-left:11px; margin-top:5px; z-index:10; }
</style>
</head>

<body onload="listen()">
<div id="container">
<canvas id="canvas" width="298" height="298"></canvas>
<canvas id="under" width="298" height="298"></canvas>
</div>
</body>

You’ll see I have two canvas elements atop one another. This is not the case in the dev.opera tutorial, but is a huge improvement on their approach.

Then we set up the listeners:

<script type="text/javascript">
var canvas,under,context,underCtx;
var points=new Array();
var iphone = ((window.navigator.userAgent.match('iPhone'))||(window.navigator.userAgent.match('iPod')))?true:false;
var ipad = (window.navigator.userAgent.match('iPad'))?true:false;
document.body.addEventListener('touchmove',function(event){ event.preventDefault(); });

function listen(){
canvas = document.getElementById('canvas');
under = document.getElementById('under');
if(canvas){
context= canvas.getContext('2d');
context.lineCap = 'round';
context.lineWidth=4;
if(iphone||ipad){
canvas.addEventListener('touchstart', strokeStart);
}
else{
canvas.addEventListener('mousedown', strokeStart);
}
}
if(under){
underCtx=under.getContext('2d');
underCtx.lineCap = 'round';
}
}

Oh, the reason you’re not seeing things like an end to the SCRIPT is that I’m walking through a file, theoretically you could grab all of the code samples, concatenate them, and have the final page (but I’ll give you that anyway, so don’t bother).

Here you see a few things: a canvas element has a context (similar to a physical canvas having pigment on it), and those have attributes like line width, how line ends are handled, and so on. So I set up the different JS variables to match with the HTML elements and their contexts. I also use this:

document.body.addEventListener('touchmove',function(event){ event.preventDefault(); });


So that when something would have moved the body of the page, it doesn’t respond (if I’m dragging my finger, I don’t want it to move the page, but instead, want it to be recognized as drawing).

Then I test for iOS devices (that’s the market my app will initially target, so you’d want to amend this if you’re looking at Android), and begin setting up the event listeners. If it’s iOS, I use touch events, otherwise, mouse events (assuming it’s a browser based user…if this were really going to be distributed through iTunes [via a PhoneGap type wrapper] you could take those conditional elements out since you’d KNOW touch events would be required).

For those of you that aren’t familiar with the syntax:

canvas.addEventListener('touchstart', strokeStart);

Means: “for canvas, if the touchstart event occurs, fire the strokeStart function”

So, what’s in that function?

function strokeStart(ev) {
ev.preventDefault();
canvas.addEventListener('touchend', strokeEnd);
canvas.addEventListener('touchmove', stroke);
canvas.addEventListener('mousemove', stroke);
canvas.addEventListener('mouseup',   strokeEnd);
var a=new Object();
if (((iphone)||(ipad))&&(ev.touches[0])){ //iPad
a.x = ev.touches[0].clientX;
a.y = ev.touches[0].clientY;
}
else if (ev.layerX || ev.layerX == 0) { // Firefox
a.x = ev.layerX;
a.y = ev.layerY;
}
else if (ev.offsetX || ev.offsetX == 0) { // Opera
a.x = ev.offsetX;
a.y = ev.offsetY;
}
points.push(a);
}

So, I’m adding OTHER listeners (touchmove, touchend) and grabbing the coordinates of where that touch occurred, and then adding it to the points array. Why? You’ll see in just a bit.

First let’s look at what the other listeners are doing:

function stroke(ev) {
var a=new Object();
if (((iphone)||(ipad))&&(ev.touches[0])){ //iPad
a.x = ev.touches[0].clientX;
a.y = ev.touches[0].clientY;
}
else if (ev.layerX || ev.layerX == 0) { // Firefox
a.x = ev.layerX;
a.y = ev.layerY;
}
else if (ev.offsetX || ev.offsetX == 0) { // Opera
a.x = ev.offsetX;
a.y = ev.offsetY;
}
points.push(a);
context.clearRect(0, 0, canvas.width, canvas.height);
drawCurveStroke(context);
}

function strokeEnd() {
canvas.removeEventListener('touchend', strokeEnd, false);
canvas.removeEventListener('touchmove', stroke, false);
canvas.removeEventListener('mousemove', stroke, false);
canvas.removeEventListener('mouseup',   strokeEnd, false);
context.clearRect(0, 0, canvas.width, canvas.height);
//these three lines make sure the lower canvas has the same settings as the top canvas
underCtx.lineWidth = context.lineWidth;
underCtx.strokeStyle = context.strokeStyle;
underCtx.fillStyle = context.fillStyle;
drawCurveStroke(underCtx);
points = [];
}

So the stroke function (which fires each time the mouse/touch moves) grabs another coordinate, adds it to the points array, clears the context (of the top canvas) and then calls the drawCurveStroke function (with the context as a parameter). strokeEnd (which fires when the mouse button or finger lifts) removes some listeners, clears the context of the top canvas, and then calls the drawCurveStroke (with the underCtx as the parameter).

So what is going on? In the dev.opera example, there is just a single canvas, and each time a new coordinate is added to the drawing, all of of the line segments connecting preceding coordinates are re-stroked, causing a bit of graininess on the edges of the lines. What we’re doing instead is continually clearing the context, so that the line we’re drawing is only stroked one time (using the values in the points array). And then, when we lift our finger, that line is copied onto the lower canvas, so that the points array is only dealing with the current line being drawn.

Here is the drawCurveStroke function, I believe the core of it was from sketchfemme.com but I really can’t find the exact nugget, that was something the other developer dug up:

function  drawCurveStroke(context) {
if (points.length < 1) {
return;
}
if (points.length < 6) {
var point = points[0];
context.beginPath();
context.arc(point.x, point.y,parseInt(context.lineWidth)/2, 0, Math.PI * 2, true);
context.closePath();
context.fill();
return;
}
context.beginPath();
// move to the first point
context.moveTo(points[0].x, points[0].y);
// curve through the rest, stopping at each midpoint
for (i = 1; i < points.length - 2; i ++)
{
var xc = (points[i].x + points[i + 1].x) / 2;
var yc = (points[i].y + points[i + 1].y) / 2;
context.quadraticCurveTo(points[i].x, points[i].y, xc, yc);
}
context.quadraticCurveTo(points[i].x, points[i].y, points[i+1].x,points[i+1].y);
context.stroke();
}

So the function determines how many points exist in the array, and then uses quadraticCurveTo between them if enough exist.

As promised, here is the consolidated page, you’ll see that as you draw, the lines have a much more fluid aesthetic, particularly when compared to the rough lines in the dev.opera link (which reminds me of some sort of Apple IIGS paint program). Feel free to visit in your iOS device as well.

I’ll leave it to you to add other controls, you’ll want those to change the various settings of the upper canvas, which will then be mirrored to the lower on strokeEnd.

Working in Android? What changes are you making to the code to make it work? And, given the success of DrawSomething, what are your thoughts on other drawing apps (live, or concepts that could see the light of day)?

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>