scratchcard
Version:
Mimic a scratchcard with HTML5
450 lines (345 loc) • 10.1 kB
JavaScript
/* jshint browserify: true */
;
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var defaults = require('lodash.defaults');
var Painter = Scratchcard.Painter = require('./painter');
module.exports = exports = Scratchcard;
var defaultOptions = {
threshold: 255,
realtime: false,
layers: 0,
painter: {},
animationFrame: null
};
function Scratchcard(element, options) {
if (isIeLessThan9()) {
throw new Error('Internet Explorer not supported prior to version 9');
}
EventEmitter.call(this);
var self = this;
var canvas = document.createElement('canvas');
if (!canvas.getContext) {
throw new Error('HTML5 canvas not supported');
}
var ctx = canvas.getContext('2d');
options = (options instanceof Painter) ?
defaults({painter: options}, defaultOptions) :
defaults({}, options, defaultOptions);
var requestAnimationFrame = window.requestAnimationFrame;
var animationFrame = options.animationFrame;
if (animationFrame && animationFrame.request) {
requestAnimationFrame = animationFrame.request.bind(animationFrame);
}
if (!requestAnimationFrame) {
throw new Error('requestAnimationFrame not supported');
}
var size = getSize(element);
canvas.style.position = 'absolute';
canvas.width = size.width;
canvas.height = size.height;
canvas.style.zIndex = options.layers + 1;
// Disable the blue overlay for some browsers
canvas.style['-webkit-tap-highlight-color'] = 'rgba(0,0,0,0)';
var wrapper = document.createElement('div');
wrapper.className = 'scratchcard';
wrapper.style.position = 'relative';
wrapper.style.display = 'inline';
wrapper.style.verticalAlign = 'top';
wrapper.appendChild(canvas);
var layers = [];
for (var index = 0; index < options.layers; index++) {
var layer = document.createElement('canvas');
layer.style.position = 'absolute';
layer.width = size.width;
layer.height = size.height;
layer.style.zIndex = index + 1;
layer.style.pointerEvents = 'none';
layers.push({
canvas: layer,
ctx: layer.getContext('2d')
});
wrapper.appendChild(layer);
}
var painter = null;
var previousFingers = {};
var previousFingersCount = 0;
var currentFingers = {};
var currentFingersCount = 0;
var updated = false;
var progress = null;
var completed = false;
var animated = false;
this.getElement = getElement;
this.setElement = setElement;
this.getPainter = getPainter;
this.setPainter = setPainter;
this.getWrapper = getWrapper;
this.getCanvas = getCanvas;
this.reset = reset;
this.complete = complete;
this.getProgress = getProgress;
setPainter(options.painter);
if (element) {
element.parentNode.insertBefore(wrapper, element);
element.style.visibility = 'visible';
canvas.addEventListener('mousedown', onMouseDown, true);
canvas.addEventListener('touchstart', onTouchStart, true);
animate();
}
function getElement() {
return element;
}
function setElement(newElement) {
if (newElement) {
element = newElement;
synchronize();
canvas.addEventListener('mousedown', onMouseDown, true);
canvas.addEventListener('touchstart', onTouchStart, true);
if (!animated) {
animate();
}
} else if (element) {
element = null;
wrapper.parentNode.removeChild(wrapper);
canvas.removeEventListener('mousedown', onMouseDown, true);
canvas.removeEventListener('touchstart', onTouchStart, true);
}
}
function getPainter() {
return painter;
}
function setPainter(newPainter) {
painter = (newPainter instanceof Painter) ? newPainter : new Painter(newPainter);
redraw();
}
function getWrapper() {
return wrapper;
}
function getCanvas() {
return canvas;
}
function reset() {
painter.reset(ctx, canvas.width, canvas.height);
updated = true;
completed = false;
canvas.style.pointerEvents = null;
checkProgress();
}
function drawPoint(point) {
painter.drawPoint(ctx, point);
updated = true;
}
function drawLine(start, end) {
painter.drawLine(ctx, start, end);
updated = true;
}
function complete() {
painter.complete(ctx, canvas.width, canvas.height);
updated = true;
completed = true;
canvas.style.pointerEvents = 'none';
checkProgress();
}
function redraw() {
if (completed) {
painter.complete(ctx, canvas.width, canvas.height);
} else {
painter.reset(ctx, canvas.width, canvas.height);
}
updated = true;
for (var index = 0, count = layers.length; index < count; index++) {
var layer = layers[index];
painter.drawLayer(layer.ctx, layer.canvas.width, layer.canvas.height, index);
}
checkProgress();
}
function getProgress() {
var width = canvas.width;
var height = canvas.height;
var pixels = width * height;
if (pixels === 0) {
return 0;
}
var holes = 0;
var data = ctx.getImageData(0, 0, width, height).data;
for (var index = 3, count = data.length; index < count; index += 4) {
if (data[index] >= options.threshold) {
holes++;
}
}
return (pixels - holes) / pixels;
}
function checkProgress() {
if (!updated) {
return;
}
var lastProgress = progress;
progress = getProgress();
updated = false;
if (progress !== lastProgress) {
self.emit('progress', progress);
if ((progress === 1) && (!completed)) {
complete();
}
}
}
function synchronize() {
var parent = element.parentNode;
if (!parent) {
setElement(null);
return;
}
if (wrapper.nextSibling !== element) {
parent.insertBefore(wrapper, element);
}
var size = getSize(element);
if ((canvas.width === size.width) && (canvas.height === size.height)) {
return;
}
canvas.width = size.width;
canvas.height = size.height;
for (var index = 0, count = layers.length; index < count; index++) {
var layer = layers[index].canvas;
layer.width = size.width;
layer.height = size.height;
}
redraw();
}
function animate() {
if (!element) {
animated = false;
return;
}
requestAnimationFrame(animate);
animated = true;
synchronize();
for (var identifier in currentFingers) {
var previousFinger = previousFingers[identifier];
var currentFinger = currentFingers[identifier];
if (!previousFinger) {
drawPoint(currentFinger);
} else if ((currentFinger.x !== previousFinger.x) || (currentFinger.y !== previousFinger.y)) {
drawLine(previousFinger, currentFinger);
}
}
if (options.realtime || ((currentFingersCount === 0) && (previousFingersCount > 0))) {
checkProgress();
}
previousFingers = currentFingers;
previousFingersCount = currentFingersCount;
}
function onMouseDown(event) {
if (event.button !== 0) {
return;
}
event.preventDefault();
// console.log('mousedown');
var boundingRect = canvas.getBoundingClientRect();
currentFingers = {
mouse: {
x: event.clientX - boundingRect.left,
y: event.clientY - boundingRect.top
}
};
currentFingersCount = 1;
window.addEventListener('mousemove', onMouseMove, true);
window.addEventListener('mouseup', onMouseUp, true);
canvas.removeEventListener('mousedown', onMouseDown, true);
canvas.removeEventListener('touchstart', onTouchStart, true);
}
function onMouseMove(event) {
if (event.button !== 0) {
return;
}
event.preventDefault();
// console.log('mousemove');
var boundingRect = canvas.getBoundingClientRect();
currentFingers = {
mouse: {
x: event.clientX - boundingRect.left,
y: event.clientY - boundingRect.top
}
};
currentFingersCount = 1;
}
function onMouseUp(event) {
if (event.button !== 0) {
return;
}
event.preventDefault();
// console.log('mouseup');
currentFingers = {};
currentFingersCount = 0;
window.removeEventListener('mousemove', onMouseMove, true);
window.removeEventListener('mouseup', onMouseUp, true);
if (element) {
canvas.addEventListener('mousedown', onMouseDown, true);
canvas.addEventListener('touchstart', onTouchStart, true);
}
}
function onTouchStart(event) {
event.preventDefault();
// console.log('touchstart');
currentFingers = {};
currentFingersCount = event.touches.length;
var boundingRect = canvas.getBoundingClientRect();
for (var index = 0; index < currentFingersCount; index++) {
var touch = event.touches[index];
currentFingers[touch.identifier] = {
x: touch.clientX - boundingRect.left,
y: touch.clientY - boundingRect.top
};
}
window.addEventListener('touchstart', onTouch, true);
window.addEventListener('touchmove', onTouch, true);
window.addEventListener('touchend', onTouch, true);
canvas.removeEventListener('mousedown', onMouseDown, true);
canvas.removeEventListener('touchstart', onTouchStart, true);
}
function onTouch(event) {
event.preventDefault();
// console.log(event.type);
currentFingers = {};
currentFingersCount = event.touches.length;
if (currentFingersCount > 0) {
var boundingRect = canvas.getBoundingClientRect();
for (var index = 0; index < currentFingersCount; index++) {
var touch = event.touches[index];
currentFingers[touch.identifier] = {
x: touch.clientX - boundingRect.left,
y: touch.clientY - boundingRect.top
};
}
} else {
window.removeEventListener('touchstart', onTouch, true);
window.removeEventListener('touchmove', onTouch, true);
window.removeEventListener('touchend', onTouch, true);
if (element) {
canvas.addEventListener('mousedown', onMouseDown, true);
canvas.addEventListener('touchstart', onTouchStart, true);
}
}
}
}
util.inherits(Scratchcard, EventEmitter);
function isIeLessThan9() {
var div = document.createElement('div');
div.innerHTML = '<!--[if lt IE 9]><i></i><![endif]-->';
return (div.getElementsByTagName('i').length === 1);
}
var zero = {width: 0, height: 0};
function getSize(element) {
if (!element) {
return zero;
}
var rects = element.getClientRects();
if (rects.length === 0) {
return zero;
}
var firstRect = rects[0];
return {
width: Math.ceil(firstRect.right) - Math.floor(firstRect.left),
height: Math.ceil(firstRect.bottom) - Math.floor(firstRect.top)
};
}