Created
December 8, 2013 15:13
-
-
Save elin-moco/7858787 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Scratch-off canvas | |
* | |
* NOTE: this code monkeypatches Function.prototype.bind() if it doesn't | |
* already exist. | |
* | |
* NOTE: This is demo code that has been converted to be less demo-y. | |
* But it is still demo-y. | |
* | |
* To make (more) correct: | |
* o add error handling on image loads | |
* o fix inefficiencies | |
* | |
* depends on jQuery>=1.7 | |
* | |
* Copyright (c) 2012 Brian "Beej Jorgensen" Hall <beej@beej.us> | |
* | |
* Permission is hereby granted, free of charge, to any person obtaining | |
* a copy of this software and associated documentation files (the | |
* "Software"), to deal in the Software without restriction, including | |
* without limitation the rights to use, copy, modify, merge, publish, | |
* distribute, sublicense, and/or sell copies of the Software, and to | |
* permit persons to whom the Software is furnished to do so, subject to | |
* the following conditions: | |
* | |
* The above copyright notice and this permission notice shall be | |
* included in all copies or substantial portions of the Software. | |
* | |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | |
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY | |
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, | |
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE | |
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
*/ | |
Scratcher = (function () { | |
/** | |
* Helper function to extract the coordinates from an event, whether the | |
* event is a mouse or touch. | |
*/ | |
function getEventCoords(ev) { | |
var first, coords = {}; | |
var origEv = ev.originalEvent; // get from jQuery | |
if (origEv.changedTouches != undefined) { | |
first = origEv.changedTouches[0]; | |
coords.pageX = first.pageX; | |
coords.pageY = first.pageY; | |
} else { | |
coords.pageX = ev.pageX; | |
coords.pageY = ev.pageY; | |
} | |
return coords; | |
}; | |
/** | |
* Helper function to get the local coords of an event in an element. | |
* | |
* @param elem element in question | |
* @param ev the event | |
*/ | |
function getLocalCoords(elem, coords) { | |
var offset = $(elem).offset(); | |
return { | |
'x': coords.pageX - offset.left, | |
'y': coords.pageY - offset.top | |
}; | |
}; | |
/** | |
* Construct a new scratcher object | |
* | |
* @param canvasId [string] the canvas DOM ID, e.g. 'canvas2' | |
* @param backImage [string, optional] URL to background (bottom) image | |
* @param frontImage [string, optional] URL to foreground (top) image | |
*/ | |
function Scratcher(canvasId, backImage) { | |
this.canvas = { | |
'main': $('#' + canvasId).get(0) | |
}; | |
this.fresh = true; | |
this.mouseDown = true; | |
this.canvasId = canvasId; | |
this._setupCanvases(); // finish setup from constructor now | |
this.setImages(backImage); | |
this._eventListeners = {}; | |
}; | |
/** | |
* Set the images to use | |
*/ | |
Scratcher.prototype.setImages = function (backImage) { | |
this.image = { | |
'back': { 'url': backImage, 'img': null } | |
}; | |
if (backImage) { | |
this._loadImages(); // start image loading from constructor now | |
} | |
}; | |
/** | |
* Returns how scratched the scratcher is | |
* | |
* By adjusting the stride, you get a less accurate result, but it is | |
* quicker to compute (pixels are skipped) | |
* | |
* @param stride [optional] pixel step value, default 1 | |
* | |
* @return the fraction the canvas has been scratched (0.0 -> 1.0) | |
*/ | |
Scratcher.prototype.fullAmount = function (stride) { | |
var i, l; | |
var can = this.canvas.main; | |
var ctx = can.getContext('2d'); | |
var count, total; | |
var pixels, pdata; | |
if (!stride || stride < 1) { | |
stride = 1; | |
} | |
stride *= 4; // 4 elements per pixel | |
pixels = ctx.getImageData(0, 0, can.width, can.height); | |
pdata = pixels.data; | |
l = pdata.length; // 4 entries per pixel | |
total = (l / stride) | 0; | |
for (i = count = 0; i < l; i += stride) { | |
if (pdata[i] != 0) { | |
count++; | |
} | |
} | |
return count / total; | |
}; | |
/** | |
* Recomposites the canvases onto the screen | |
* | |
* Note that my preferred method (putting the background down, then the | |
* masked foreground) doesn't seem to work in FF with "source-out" | |
* compositing mode (it just leaves the destination canvas blank.) I | |
* like this method because mentally it makes sense to have the | |
* foreground drawn on top of the background. | |
* | |
* Instead, to get the same effect, we draw the whole foreground image, | |
* and then mask the background (with "source-atop", which FF seems | |
* happy with) and stamp that on top. The final result is the same, but | |
* it's a little bit weird since we're stamping the background on the | |
* foreground. | |
* | |
* OPTIMIZATION: This naively redraws the entire canvas, which involves | |
* four full-size image blits. An optimization would be to track the | |
* dirty rectangle in scratchLine(), and only redraw that portion (i.e. | |
* in each drawImage() call, pass the dirty rectangle as well--check out | |
* the drawImage() documentation for details.) This would scale to | |
* arbitrary-sized images, whereas in its current form, it will dog out | |
* if the images are large. | |
*/ | |
Scratcher.prototype.recompositeCanvases = function () { | |
var can = this.canvas.main; | |
var ctx = can.getContext('2d'); | |
ctx.globalCompositeOperation = 'copy'; | |
ctx.drawImage(this.image.back.img, 0, 0); | |
ctx.globalCompositeOperation = 'destination-out'; | |
}; | |
/** | |
* Draw a scratch line | |
* | |
* Dispatches the 'scratch' event. | |
* | |
* @param x,y the coordinates | |
* @param fresh start a new line if true | |
*/ | |
Scratcher.prototype.scratchLine = function (x, y) { | |
var can = this.canvas.main; | |
var ctx = can.getContext('2d'); | |
ctx.lineWidth = 30; | |
ctx.lineCap = ctx.lineJoin = 'round'; | |
ctx.strokeStyle = 'rgba(0,0,0,1)'; // can be any opaque color | |
if (this.fresh) { | |
ctx.beginPath(); | |
this.fresh = false; | |
// this +0.01 hackishly causes Linux Chrome to draw a | |
// "zero"-length line (a single point), otherwise it doesn't | |
// draw when the mouse is clicked but not moved: | |
ctx.moveTo(x + 0.01, y); | |
} | |
ctx.lineTo(x, y); | |
ctx.stroke(); | |
// call back if we have it | |
this.dispatchEvent(this.createEvent('scratch')); | |
}; | |
/** | |
* Set up the main canvas and listeners | |
*/ | |
Scratcher.prototype._setupCanvases = function () { | |
var c = this.canvas.main; | |
// create the temp and draw canvases, and set their dimensions | |
// to the same as the main canvas: | |
/** | |
* On mouse move, if mouse down, draw a line | |
* | |
* We do this on the window to smoothly handle mousing outside | |
* the canvas | |
*/ | |
function mousemove_handler(e) { | |
if (!this.mouseDown) { | |
return true; | |
} | |
var local = getLocalCoords(c, getEventCoords(e)); | |
this.scratchLine(local.x, local.y); | |
return false; | |
}; | |
$(document).on('mousemove', mousemove_handler.bind(this)); | |
$(document).on('touchmove', mousemove_handler.bind(this)); | |
}; | |
/** | |
* Reset the scratcher | |
* | |
* Dispatches the 'reset' event. | |
* | |
*/ | |
Scratcher.prototype.reset = function () { | |
// clear the draw canvas | |
this.fresh = true; | |
this.recompositeCanvases(); | |
// call back if we have it | |
this.dispatchEvent(this.createEvent('reset')); | |
}; | |
/** | |
* returns the main canvas jQuery object for this scratcher | |
*/ | |
Scratcher.prototype.mainCanvas = function () { | |
return this.canvas.main; | |
}; | |
/** | |
* Handle loading of needed image resources | |
* | |
* Dispatches the 'imagesloaded' event | |
*/ | |
Scratcher.prototype._loadImages = function () { | |
var loadCount = 0; | |
// callback for when the images get loaded | |
function imageLoaded(e) { | |
loadCount++; | |
if (loadCount >= 1) { | |
// call the callback with this Scratcher as an argument: | |
this.dispatchEvent(this.createEvent('imagesloaded')); | |
this.recompositeCanvases(); | |
} | |
} | |
// load BG and FG images | |
for (k in this.image) if (this.image.hasOwnProperty(k)) { | |
this.image[k].img = document.createElement('img'); // image is global | |
$(this.image[k].img).on('load', imageLoaded.bind(this)); | |
this.image[k].img.src = this.image[k].url; | |
} | |
}; | |
/** | |
* Create an event | |
* | |
* Note: not at all a real DOM event | |
*/ | |
Scratcher.prototype.createEvent = function (type) { | |
var ev = { | |
'type': type, | |
'target': this, | |
'currentTarget': this | |
}; | |
return ev; | |
}; | |
/** | |
* Add an event listener | |
*/ | |
Scratcher.prototype.addEventListener = function (type, handler) { | |
var el = this._eventListeners; | |
type = type.toLowerCase(); | |
if (!el.hasOwnProperty(type)) { | |
el[type] = []; | |
} | |
if (el[type].indexOf(handler) == -1) { | |
el[type].push(handler); | |
} | |
}; | |
/** | |
* Remove an event listener | |
*/ | |
Scratcher.prototype.removeEventListener = function (type, handler) { | |
var el = this._eventListeners; | |
var i; | |
type = type.toLowerCase(); | |
if (!el.hasOwnProperty(type)) { | |
return; | |
} | |
if (handler) { | |
if ((i = el[type].indexOf(handler)) != -1) { | |
el[type].splice(i, 1); | |
} | |
} else { | |
el[type] = []; | |
} | |
}; | |
/** | |
* Dispatch an event | |
*/ | |
Scratcher.prototype.dispatchEvent = function (ev) { | |
var el = this._eventListeners; | |
var i, len; | |
var type = ev.type.toLowerCase(); | |
if (!el.hasOwnProperty(type)) { | |
return; | |
} | |
len = el[type].length; | |
for (i = 0; i < len; i++) { | |
el[type][i].call(this, ev); | |
} | |
}; | |
/** | |
* Set up a bind if you don't have one | |
* | |
* Notably, Mobile Safari and the Android web browser are missing it. | |
* IE8 doesn't have it, but <canvas> doesn't work there, anyway. | |
* | |
* From MDN: | |
* | |
* https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind#Compatibility | |
*/ | |
if (!Function.prototype.bind) { | |
Function.prototype.bind = function (oThis) { | |
if (typeof this !== "function") { | |
// closest thing possible to the ECMAScript 5 internal | |
// IsCallable function | |
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); | |
} | |
var aArgs = Array.prototype.slice.call(arguments, 1), | |
fToBind = this, | |
fNOP = function () { | |
}, | |
fBound = function () { | |
return fToBind.apply(this instanceof fNOP | |
? this | |
: oThis || window, | |
aArgs.concat(Array.prototype.slice.call(arguments))); | |
}; | |
fNOP.prototype = this.prototype; | |
fBound.prototype = new fNOP(); | |
return fBound; | |
}; | |
} | |
return Scratcher; | |
})(); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment