2012-04-23; 2012-08-06
HTML Canvas Scratcher, Multiple Canvases
Your browser doesn't support HTML canvas. Get a modern browser such as Chrome, Firefox, Opera, or IE9. And then, only then, will you be able to see this demo. Sorry!
In an earlier post, I
talked about how to make a "scratch off" effect using HTML canvas and
globalCompositeOperation
. Since it was just a demo and in the interest
of code clarity, it only worked on one canvas on the page. A couple of
you asked how to make it work with multiple canvases on the same place,
so I thought I'd talk about how to refactor and generalize the code.
The Old Way: one big global object closely tied to one canvas and one web page. Difficult to make work with multiple canvases.
In the old code, there was just one set of variables storing information about the one single canvas on the page. Having multiple canvases on the page wouldn't work, since they would use the same variables, overwrite each other's values, and step on each other's toes. War would break out, peace agreements would need to be brokered, and all kinds of badness would ensue.
Since it's easier to act unilaterally than get permission, let's just fix this so it works in multiple places.
The Better Way: individual scratcher object instances point to individual canvases, and are not closely tied to the page.
The problem is, as we've said, that multiple canvases would conflict when they used the same variables as one another. The obvious solution is to give each canvas its own set of variables to use in isolation. But they should all be able to use the same functions and code to manipulate themselves, since they all perform the same function, just with different data.
So we need an object that holds data for a specific canvas, plus has all the functionality needed for manipulating that canvas.
This is a perfect setup for using classes and objects. JavaScript technically doesn't have classes, but it does have objects that can be created with constructors, so it's close enough for government work.
Step 1: Identify Utility Functions
In this code, there are functions that are helper/utility functions that
don't need to be in the scratcher object. For example,
supportsCanvas()
returns true
if this browser supports <canvas>
.
This is a general utility function that doesn't have anything to do with
the scratcher, so it doesn't need to be in the scratcher object.
There might also be global variables that are similar. Move these out of the way, since they likely won't change during the conversion. I just moved them to the top of this code, but it might make more sense to break them out into a utility library.
Similar functions in this code are getEventCoords()
and
getLocalCoords()
.
Step 2: Move Globals to Constructor
Let's identify all those global variables that are specific to the scratcher, and move them all into a constructor:
var image = { // back and front images
'back': { 'url':'images/background.jpg', 'img':null },
'front': { 'url':'images/foreground.jpg', 'img':null }
};
var canvas = {'temp':null, 'draw':null}; // temp and draw canvases
var mouseDown = false;
That's not too bad; only a few of them. We'll make a new constructor for holding them in objects by simply moving them out the global space and making them members of the new object. Since the background and foreground images will be different for different scratchers, we'll pass those into the constructor as parameters.
There's one more thing to consider, and that's functions that reference
global variables or DOM objects. For example, we have the following
old code in recompositeCanvases()
:
var main = $('#maincanvas').get(0);
It's referring to the one single DOM canvas element with ID
#maincanvas
. This is bad, since we want each scratcher object to
refer to its own canvas. So like the foreground and background images,
we'll pass a canvas into the scratcher constructor. (We could pass the
canvas as a DOM element reference or a jQuery
selector result, but we'll just pass it as a string with the canvas's
ID. Depending on the application, some of these approaches might be
easier than others.)
Other additions are an on-load callback, and the fact that we're calling
setupCanvases()
from inside the constructor now. We'll talk more
about the callback later.
Here's the new constructor:
function Scratcher(canvasId, backImage, frontImage, onLoadCallback) {
this.image = {
'back': { 'url':backImage, 'img':null },
'front': { 'url':frontImage, 'img':null }
};
this.canvas = {'temp':null, 'draw':null}; // temp and draw canvases
this.mouseDown = false;
this.canvasId = canvasId;
this.onLoadCallback = onLoadCallback; // call this when images loaded
this.setupCanvases(); // finish setup from constructor now
this.loadImages(); // start image loading from constructor now
};
And now we can make new ones like this (here shown without the optional
onLoadCallback
argument):
var scratcher1 = new Scratcher('can1', 'fgimage1.jpg', 'bgimage1.jpg');
var scratcher2 = new Scratcher('can2', 'fgimage2.jpg', 'bgimage2.jpg');
(Note: the constructor would be a great place to set the drawing line width!)
But we're not nearly done yet—we still need to link the functionality to
the Scratcher
objects.
Step 3: Move Functions to Methods
Now might be a good time to take a refresher course on how prototypes work in JavaScript, since that's how we're going to add methods to our objects.
Here are the first couple lines of the old recompositeCanvases()
function:
recompositeCanvases() {
var main = $('#maincanvas').get(0);
...
There are two problems with it. One is that it's a function, not a
method attached to the Scratcher
object. The other is that it
references the DOM element #maincanvas
, which has been moved into the
Scratcher
object as the canvasId
property.
We need to convert it to a method, and fix the reference. We make it a
method by adding it to Scratcher
's prototype object, and fix the
canvas ID reference like so:
Scratcher.prototype.recompositeCanvases = function() {
var main = $('#' + this.canvasId).get(0);
...
Efficiency! Do we need to look up the canvas DOM element by ID every time we call
recompositeCanvas()
? It never changes, so no, we don't. It would be more efficient to look it up once in the constructor, and then just refer to it in this method. The current code is rife with such issues.
Now what about function scratchLine()
? In a way this is a generic
function that draws a line on a canvas. It's broken out into a function
in the interest of
DRY, because it
gets called in two different places. But should it be a method or a
utility function?
This is one of those gray areas, but I'm going to say that since it is only called from Scratcher code, it'll be a private method in the object. (Another option could be to make it a private function within the method that uses it.)
Since scratchLine()
only draws on the draw
canvas, there's no reason
to pass it in as an argument; we can just get it from this
since it's
stored in the object.
Step 3A: this
and Event Handlers
We're about to get to the first pesky part of this whole thing, and it
happens in the setupCanvases()
function. This function does a lot of
setup and it might be better-placed in the constructor. If not, it
should at least be called from the constructor. (Sometimes it makes
more sense to leave it as a separate method if it can be called from
multiple places.)
In the interest of quickly converting the original code, as you see in
the constructor, above, I just added a call to the end of it to jump to
the setupCanvases()
method.
We see a reference in there to #maincanvas
that has to be changed to
this.canvasId
, and every call to recompositeCanvases()
needs to be
changed to this.recompositeCanvases()
(same for scratchLine()
and
the canvas
and mousedown
variables)...
function mousedown_handler(e) {
var local = getLocalCoords(c, getEventCoords(e));
mouseDown = true;
this.scratchLine(local.x, local.y, true); // "this" BADNESS
this.recompositeCanvases(); // "this" BADNESS
return false;
};
Except for one thing! See how some of those method calls are inside
event handlers? JavaScript has a Feature where this
in an event
handler is bound to the object or DOM element firing the event! this
no longer refers to this Scratcher
object! And the DOM element
certainly doesn't have a scratchLine()
or recompositeCanvases()
method! How do we get access back to this Scratcher
object? We're
doomed! DOOOOOMED!
Ok, no actually. JavaScript provides a convenient method on each
Function called bind()
. (Not to be confused with jQuery's bind()
,
which is a completely different animal.) The bind()
method returns a
new function for which this
is bound to the object you provide. So
before we had this:
$(document).on('mousemove', mousemove_handler);
And we'll change it to this (this
refers to this Scratcher
object,
here):
$(document).on('mousemove', mousemove_handler.bind(this));
And now this
in the handler will refer to what we've specified in the
bind()
call.
bind()
is a relatively newer feature in JavaScript (spec'd in 2009), and some current browsers still do not support it (e.g. Mobile Safari and Android's stock Web Browser, and IE8 and below.)One option is to monkey patch your own
bind()
, and add it toFunction
's prototype. The Mozilla Developer's Network has exactly such an example. (I do this in this demo.)Monkey patching as a concept has started a number of spirited articles and discussions [↑] [↑] [↑] . I'm of the opinion that it's not so bad in a limited and expected sense, but if you want to avoid it all together, there are other approaches you can take, as well.
I use jQuery for this demo, so we could do it in a jQuery style. The jQuery
on()
method allows you to pass arbitrary data to the handler, and we could pass a reference to this canvas in there:$(document).on('mousedown', null, this, mousedown_handler);
And then in the handler, we can extract it from the event's
data
property, and use it:function mousedown_handler(e) { var thisScratcher = e.data; // <-- HERE var local = getLocalCoords(c, getEventCoords(e)); thisScratcher.mouseDown = true; thisScratcher.scratchLine(local.x, local.y, true); thisScratcher.recompositeCanvases(); return false; };
And finally, there's the classic way of doing things, which I'll cover since it's pretty common. We're going to use something called a closure to make this
Scratcher
available in the handler function. And we do it like so:Scratcher.prototype.setupCanvases = function() { var thisScratcher = this; // "this" is a Scratcher function mousedown_handler(e) { var local = getLocalCoords(c, getEventCoords(e)); thisScratcher.mouseDown = true; thisScratcher.scratchLine(local.x, local.y, true); thisScratcher.recompositeCanvases(); return false; };
The closure makes the variables in the enclosing block available in local functions. So even though
thisScratcher
isn't declared inmousedown_handler()
, it can still be referred to from within that function.Even though
this
inmousedown_handler()
is bound to the DOM element creating the event,thisScratcher
has been assigned a reference to theScratcher
object in question. Andmousedown_handler()
can refer to that.One gotcha with this last approach is that
mousedown_handler()
holds a reference to theScratcher
instance. If you ever need the instance to be garbage collected, all references must be dropped. This means you need to unbind (or call.off()
in jQuery) all the event handlers that refer to thethisScratcher
so that the reference is dropped.
For this conversion, I'll just go ahead and use the bind()
call; since
we're using <canvas>
, modern browsers are assumed, and really should
support it. Right, iOS and Android?
Step 3B: The Loading Code
This stuff is an annoyance. In the old version, we just wouldn't start the demo until the images were loaded, but now each canvas is doing its own loading of images.
Again, there are options. We could just ignore the issue, but we might be curious about when the scratchers are ready to go. (For example, do not enable the "reset" button until all scratchers are ready!)
One solution would be to use the event system, and fire a JavaScript
event (say, the load
event) from the Scratcher
object. Interested
parties could subscribe to that event. jQuery even has functions to
make this work cross-browser (see
trigger()
.)
Another jQuery-esque solution would be to use its
Deferred
mechanism
which avoids the race conditions that might occur with events.
Another common solution is to pass a
callback
into the constructor. This callback would be called when the Scratcher
object had finished loading. If the object creator was interested in
knowing when it was done, it could pass a callback. I used this method
in my code.
Step 4: Presentation Layer
Now that we have the guts reworked, we have to deal with the topmost level. This means we need to change the HTML page to have multiple canvases, and we need to change the page initialization code to deal with the new object construction and loading scheme.
I got rid of the old global "Loading" stuff, and put a new one in that would get turned off when all canvases finished loading. I can tell when this happens when all callbacks return (I simply count the number of callbacks that have occurred.)
Finally, I call new Scratcher(...)
to create scratchers attached to
canvases. I call it three times, one for each canvas in question.
Further Work
Like I said before, this was originally set up to demo a specific piece of HTML canvas functionality, and wasn't meant for production. The code here is definitely a step up, but there's more to go for proper packaging. Here are a few ideas:
- Move the Scratcher object out of and away from from page control code.
- Add error handling if image loading fails.
- Fix a number of inefficiencies in the code (it was optimized for code clarity, not speed.)
Resist the urge to blindly cut-n-paste the code; you're just selling yourself short!
That said, a gentleman in Copenhagen offered free food and pints in exchange for a function that would tell you how much of the canvas had been scratched off. Astute observers might have noticed that the lower canvas in the demo has its scratch percentage displayed below it.
So for a brief diversion, here's how that works in a nutshell. Since the drawing canvas is just red lines that you paint (see my previous blog entry for details), what we do is get all the pixels, and count the red ones. The red pixels divided by the total number of pixels is the full percentage.
Counting all the pixels can be a little bit hard on the CPU for large canvases on underpowered devices. To overcome this, we can just count every _n_th pixel instead of every pixel. Of course, this is less accurate, but can be much faster, and might be good enough for government work.
So I've added a
fullAmount()
method to theScratcher
. It takes an optional parameter of the number of pixels to skip (i.e. the n from "every _n_th pixel", above), the value of which defaults to 1 (count every pixel).(There are definitely more robust ways to do this fractional test, but none are as easy to code as this one, and this one is probably good enough.)
But how do you know when the canvas has been drawn on so you can do something with the full percentage? I opted to add an ad hoc event delivery system, very similar to the DOM event system. (It's not the same; it just looks the same, sort of.) You can listen on the
Scratcher
withaddEventListener()
, and theScratcher
dispatches the following events:
scratch
: any time any scratch occcursscratchesbegan
: on mouse downscratchesended
: on mouse upreset
: when reset is called on the scratcherimagesloaded
: when the scratcher's images have loadedSee the
scratcher3Changed
function inmain.js
for details.No, I am not usually so cheap to work for food and beer; I was planning to do this, anyway. 😀
Code and References
main.js
this page's source codeScratcher
library source code- JavaScript prototypes and inheritance—previous blog entry
- MDN on adding
bind()
to older browsers - MDN on closures
- Wikipedia page on callbacks
- jQuery
- jQuery's event
trigger()
- jQuery's
Deferred