Beej's Bit Bucket

 ⚡ Tech and Programming Fun

2012-04-23; 2012-08-06

HTML Canvas Scratcher, Multiple Canvases

Loading canvas images...


0%


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 Architecture 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 New Architecture 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);
    ...

Warning 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 to Function'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 in mousedown_handler(), it can still be referred to from within that function.

Even though this in mousedown_handler() is bound to the DOM element creating the event, thisScratcher has been assigned a reference to the Scratcher object in question. And mousedown_handler() can refer to that.

One gotcha with this last approach is that mousedown_handler() holds a reference to the Scratcher 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 the thisScratcher 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:

Resist the urge to blindly cut-n-paste the code; you're just selling yourself short!

Hint Goat 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 the Scratcher. 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 with addEventListener(), and the Scratcher dispatches the following events:

See the scratcher3Changed function in main.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

Comments

Blog  ⚡  Email beej@beej.us  ⚡  Home page