Skip to content

Instantly share code, notes, and snippets.

@wnordmann
Created March 3, 2011 03:53
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save wnordmann/852304 to your computer and use it in GitHub Desktop.
Save wnordmann/852304 to your computer and use it in GitHub Desktop.
This is a fun scratch off image page using HTML, it will be the base idea for a larger project.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html>
<head>
<title>HTML5 Canvas Scratch-off Demo</title>
<style type="text/css">
body {
font-family: sans-serif;
}
#license {
font-size: 0.6em;
width: 300px;
margin-top: 30px;
border-top: 1px solid gray;
}
#license img {
display: block;
float: left;
padding: 5px 10px 5px 0;
}
h1 {
font-weight: bold;
font-size: 1.2em;
}
.fineprint {
font-size: 0.6em;
}
.bigtext {
font-size: 5em;
}
.cell {
display: table-cell;
vertical-align: middle;
text-align: center;
}
.cell canvas {
vertical-align: middle;
}
.canthumb {
border: 1px solid #222;
}
.bigtopmargin {
margin-top: 5ex;
}
.hidden {
display: none;
}
.blink {
text-decoration: blink; /* Ha ha!! */
}
#maincanvas {
border: 1px solid #222;
cursor: pointer;
}
</style>
<script type="text/javascript">
//<![CDATA[
(function() {
var image = { // back and front images
'back': { 'url':'background.jpg', 'img':null },
'front': { 'url':'foreground.jpg', 'img':null }
};
var canvas = {'temp':null, 'draw':null}; // temp and draw canvases
var mouseDown = false;
/**
* Helper function to get the local coords of an event in an element,
* since offsetX/offsetY are apparently not entirely supported, but
* offsetLeft/offsetTop/pageX/pageY are!
*
* @param elem element in question
* @param ev the event
*/
function getLocalCoords(elem, ev) {
var ox = 0, oy = 0;
var first;
var pageX, pageY;
// Walk back up the tree to calculate the total page offset of the
// currentTarget element. I can't tell you how happy this makes me.
// Really.
while (elem != null) {
ox += elem.offsetLeft;
oy += elem.offsetTop;
elem = elem.offsetParent;
}
if (ev.hasOwnProperty('changedTouches')) {
first = ev.changedTouches[0];
pageX = first.pageX;
pageY = first.pageY;
} else {
pageX = ev.pageX;
pageY = ev.pageY;
}
return { 'x': pageX - ox, 'y': pageY - oy };
}
/**
* 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.
*/
function recompositeCanvases() {
var main = document.getElementById('maincanvas');
var tempctx = canvas.temp.getContext('2d');
var mainctx = main.getContext('2d');
// Step 1: clear the temp
canvas.temp.width = canvas.temp.width; // resizing clears
// Step 2: stamp the draw on the temp (source-over)
tempctx.drawImage(canvas.draw, 0, 0);
/* !!!! this way doesn't work on FF:
// Step 3: stamp the foreground on the temp (!! source-out mode !!)
tempctx.globalCompositeOperation = 'source-out';
tempctx.drawImage(image.front.img, 0, 0);
// Step 4: stamp the background on the display canvas (source-over)
//mainctx.drawImage(image.back.img, 0, 0);
// Step 5: stamp the temp on the display canvas (source-over)
mainctx.drawImage(canvas.temp, 0, 0);
*/
// Step 3: stamp the background on the temp (!! source-atop mode !!)
tempctx.globalCompositeOperation = 'source-atop';
tempctx.drawImage(image.back.img, 0, 0);
// Step 4: stamp the foreground on the display canvas (source-over)
mainctx.drawImage(image.front.img, 0, 0);
// Step 5: stamp the temp on the display canvas (source-over)
mainctx.drawImage(canvas.temp, 0, 0);
// This code just updates the thumbnails:
var drawthumb = document.getElementById('drawthumb');
var tempthumb = document.getElementById('tempthumb');
drawthumb.getContext('2d').drawImage(canvas.draw, 0, 0, drawthumb.width, drawthumb.height);
tempthumb.getContext('2d').drawImage(canvas.temp, 0, 0, tempthumb.width, tempthumb.height);
}
/**
* Draw a scratch line
*
* @param can the canvas
* @param x,y the coordinates
* @param fresh start a new line if true
*/
function scratchLine(can, x, y, fresh) {
var ctx = can.getContext('2d');
ctx.lineWidth = 45;
ctx.lineCap = ctx.lineJoin = 'round';
ctx.strokeStyle = '#f00'; // can be any opaque color
if (fresh) {
ctx.beginPath();
// 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();
}
/**
* Set up the main canvas and listeners
*/
function setupCanvases() {
var c = document.getElementById('maincanvas');
var backthumb = document.getElementById('backthumb');
var drawthumb = document.getElementById('drawthumb');
var tempthumb = document.getElementById('tempthumb');
var forethumb = document.getElementById('forethumb');
var thumbwidth, thumbheight;
// set the width and height of the main canvas from the first image
// (assuming both images are the same dimensions)
c.width = image.back.img.width;
c.height = image.back.img.height;
// create the temp and draw canvases, and set their dimensions
// to the same as the main canvas:
canvas.temp = document.createElement('canvas');
canvas.draw = document.createElement('canvas');
canvas.temp.width = canvas.draw.width = c.width;
canvas.temp.height = canvas.draw.height = c.height;
// figure thumbnail sizes
thumbwidth = parseInt(c.width * 0.2);
thumbheight = parseInt(c.height * 0.2);
backthumb.width = drawthumb.width = tempthumb.width =
forethumb.width = thumbwidth;
backthumb.height = drawthumb.height = tempthumb.height =
forethumb.height = thumbheight;
tempthumb.getContext('2d').globalCompositeOperation = 'copy';
drawthumb.getContext('2d').globalCompositeOperation = 'copy';
backthumb.getContext('2d').drawImage(image.back.img, 0, 0, backthumb.width, backthumb.height);
forethumb.getContext('2d').drawImage(image.front.img, 0, 0, forethumb.width, forethumb.height);
// draw the stuff to start
recompositeCanvases();
/**
* On mouse down, draw a line starting fresh
*/
function mousedown_handler(e) {
var local = getLocalCoords(c, e);
mouseDown = true;
scratchLine(canvas.draw, local.x, local.y, true);
recompositeCanvases();
if (e.cancelable) { e.preventDefault(); }
return false;
};
/**
* 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 (!mouseDown) { return true; }
var local = getLocalCoords(c, e);
scratchLine(canvas.draw, local.x, local.y, false);
recompositeCanvases();
if (e.cancelable) { e.preventDefault(); }
return false;
};
/**
* On mouseup. (Listens on window to catch out-of-canvas events.)
*/
function mouseup_handler(e) {
if (mouseDown) {
mouseDown = false;
if (e.cancelable) { e.preventDefault(); }
return false;
}
return true;
};
c.addEventListener('mousedown', mousedown_handler, false);
c.addEventListener('touchstart', mousedown_handler, false);
window.addEventListener('mousemove', mousemove_handler, false);
window.addEventListener('touchmove', mousemove_handler, false);
window.addEventListener('mouseup', mouseup_handler, false);
window.addEventListener('touchend', mouseup_handler, false);
}
/**
* Set up the DOM when loading is complete
*/
function loadingComplete() {
var loading = document.getElementById('loading');
var main = document.getElementById('main');
loading.className = 'hidden';
main.className = '';
}
/**
* Handle loading of needed image resources
*/
function loadImages() {
var loadCount = 0;
var loadTotal = 0;
var loadingIndicator;
function imageLoaded(e) {
loadCount++;
if (loadCount >= loadTotal) {
setupCanvases();
loadingComplete();
}
}
for (k in image) if (image.hasOwnProperty(k))
loadTotal++;
for (k in image) if (image.hasOwnProperty(k)) {
image[k].img = document.createElement('img'); // image is global
image[k].img.addEventListener('load', imageLoaded, false);
image[k].img.src = image[k].url;
}
}
/**
* Handle page load
*/
window.addEventListener('load', function() {
var resetButton = document.getElementById('resetbutton');
loadImages();
resetButton.addEventListener('click', function() {
// clear the draw canvas
canvas.draw.width = canvas.draw.width;
recompositeCanvases()
return false;
}, false);
}, false);
})();
//]]>
</script>
</head>
<body>
<div id="loading">Loading images<span class="blink">...</span></div>
<div class="hidden" id="main">
<h1>HTML5 Canvas Scratch-off Demo</h1>
<p>Draw on the canvas:</p>
<div><canvas id="maincanvas"></canvas></div>
<div><input id="resetbutton" type="button" value="Reset"></input></div>
<div id="innards" class="bigtopmargin">
<p>What you're seeing:</p>
<div class="cell"><canvas class="canthumb" id="forethumb"></canvas></div>
<div class="cell bigtext">+</div>
<div class="cell bigtext">(</div>
<div class="cell"><canvas class="canthumb" id="drawthumb"></canvas></div>
<div class="cell bigtext">&#x2299;</div>
<div class="cell"><canvas class="canthumb" id="backthumb"></canvas></div>
<div class="cell bigtext">=</div>
<div class="cell"><canvas class="canthumb" id="tempthumb"></canvas></div>
<div class="cell bigtext">)</div>
<p>"+" is <tt>drawImage()</tt> with
<tt>globalCompositeOperation</tt> set to <tt>'source-over'</tt>
(the default).
<br/>
"&#x2299;" is <tt>drawImage()</tt> with
<tt>globalCompositeOperation</tt> set to
<tt>'source-atop'</tt>.</p>
<p>View source to see the JavaScript.</p>
</div>
<div class="fineprint bigtopmargin">I took this lizard photo at the
<a href="http://www.desertmuseum.org/">Arizona-Senora Desert
Museum</a>.</div>
<div class="fineprint bigtopmargin"><a
href="mailto:beej@beej.us">Beej Jorgensen
&ltbeej@beej.us&gt</a></div>
</div>
<div id="license"><a rel="license"
href="http://creativecommons.org/licenses/by/3.0/"><img alt="Creative
Commons License" style="border-width:0"
src="http://i.creativecommons.org/l/by/3.0/88x31.png" /></a>This
work is licensed under a <a rel="license"
href="http://creativecommons.org/licenses/by/3.0/">Creative Commons
Attribution 3.0 Unported License</a>.</div>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment