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.
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 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.
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().
Let's identify all those global variables that are specific to the scratcher, and move them all into a constructor:
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 code in recompositeCanvases():
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.
And now we can make new ones like this (here shown without the optional onLoadCallback argument):
(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.
Here are the first couple lines of the old recompositeCanvases() function:
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:
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.
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)...
And we'll change it to this (this refers to this Scratcher object, here):
And now this in the handler will refer to what we've specified in the bind() call.
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.
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?
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!)
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.
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.
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!
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 nth 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 nth 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:
- scratch: any time any scratch occcurs
- scratchesbegan: on mouse down
- scratchesended: on mouse up
- reset: when reset is called on the scratcher
- imagesloaded: when the scratcher's images have loaded
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. :-)