Scratch-Off Reveal with HTML5 Canvas

Datetime:2016-08-23 03:01:57          Topic:          Share

I’ve previously demonstrated several “sliding” before-and-after comparators for images and video that track mouse movement from side to side. More precise “scratch-off” interfaces can be useful for comparing particular kinds of images, such as photos taken at different times in history, or comparing a sketch against a final product.

Other examples I’ve found of this technique are rather complicated and over-coded, or have framework dependencies; by comparison, the code for this technique is relatively straightforward, and written in pureJavaScript.

In effect, the result is the same as my “scribble” SVG reveal technique , but interactive, rather than playing automatically. For this example, I’ve used Calgary’s Center Street bridge, which recently celebrated its 100th anniversary, with photographs taken 70 years apart, in 1943 and 2013. The code also demonstrates Retina background images,display density detection with JavaScript, and mouse / touch / button detection.

Images and Markup

The comparison images must be the same size, or at least the same aspect ratio. The only markup is the <canvas> , with the pixel dimensions of the image(s).

<canvas id="bridge" width="750" height="465"></canvas>

The “base” image - the one that will be revealed - is placed as thebackground of the <canvas> element :

#bridge {
    display: block;
    margin: 0 auto;
    background-image: url('calgary-bridge-1943.jpg');
    background-image: -webkit-image-set(url('/calgary-bridge-1943.jpg') 1x, 
	url('calgary-bridge-1943-2x.jpg') 2x );
    background-size: cover;
    width: 100%;
    max-width: 750px;
    height: auto;
    cursor:  crosshair;
    cursor: url(circular-cursor.png) 53 53, crosshair;
}

The background uses image-set() to use the correct image for the display density on Webkit-derived browsers, and a custom cursor the size of the “brush” that will be used on the comparator.

The Script

Placed at the bottom of the page, the script consists of severalfunctions. It starts by setting thevariables to be used:

var bridge = document.getElementById("bridge"),
bridgeCanvas = bridge.getContext('2d'),
brushRadius = (bridge.width / 100) * 5,
img = new Image();
if (brushRadius < 50) { brushRadius = 50 }

The default brush radius is 5% of the width of the canvas, but we never want the radius to be less than 50px (approximately the size of the fingertip, on most displays).

img.onload = function(){  
    bridgeCanvas.drawImage(img, 0, 0, bridge.width, bridge.height);
}
img.loc = '';
img.filename = 'calgary-bridge-2013.jpg';
if (window.devicePixelRatio >= 2) {
    var nameParts = img.filename.split('.');
    img.src = img.loc + nameParts[0]+"-2x"+"."+nameParts[1];
} else {
    img.src = img.loc + img.filename;
}

The “cover” image is drawn on the canvas, over the background image. The image is determined from combining image.loc and the base filename; -2x is appended to the filename if the screen of the device is a Retina display.

Detecting mousemove and touch on the <canvas> is achieved in two event listeners:

bridge.addEventListener("mousemove", function(e) {
    var brushPos = getBrushPos(e.clientX, e.clientY);
    var leftBut = detectLeftButton(e);
    if (leftBut == 1) {
        drawDot(brushPos.x, brushPos.y);
    }
}, false);

bridge.addEventListener("touchmove", function(e) {
    e.preventDefault();
    var touch = e.targetTouches[0];
    if (touch) {
        var brushPos = getBrushPos(touch.pageX, touch.pageY);
        drawDot(brushPos.x, brushPos.y);
    }
}, false);

Since the locations of touch and mouse movement are measured separately, the getBrushPos function is fed two different kinds of x and y position, rendering a result for the drawDot function.

I wanted the “painting” action to be active , meaning that the left button on a mouse (or the equivalent on a trackpad) needs to be held down during the draw. Detecting the left mouse button is notoriously tricky, so I used this function to resolve it:

function detectLeftButton(event) {
    if ('buttons' in event) {
        return event.buttons === 1;
    } else if ('which' in event) {
        return event.which === 1;
    } else {
        return event.button === 1;
    }
}

Getting the touch location is also a little tricky, since the <canvas> isresponsive:

function getBrushPos(xRef, yRef) {
    var bridgeRect = bridge.getBoundingClientRect();
    return {
      x: Math.floor((xRef - bridgeRect.left) / (bridgeRect.right - bridgeRect.left) * bridge.width),
      y: Math.floor((yRef - bridgeRect.top) / (bridgeRect.bottom - bridgeRect.top) * bridge.height)
    };
}

To expose the photograph underneath, a circular “dot” is drawn on the canvas with a globalCompositeOperation . Because of that, it’s not important what color the dot is filled with, only that it has a fill:

function drawDot(mouseX,mouseY){
    bridgeCanvas.beginPath();
    bridgeCanvas.arc(mouseX, mouseY, brushRadius, 0, 2*Math.PI, true);
    bridgeCanvas.fillStyle = '#000';
    bridgeCanvas.globalCompositeOperation = "destination-out";
    bridgeCanvas.fill();
}

Compatibility

This comparator works well in all modern browsers I’ve tested in. IE10 has an issue with scaling the <canvas> that needs further investigation, but it works very well in IE 11 and Edge.

Photographs by KingsdudeDave and davebloggs007

Enjoy this piece? I invite you to follow me at twitter.com/dudleystorey to learn more.
Check out the CodePen demo for this article at https://codepen.io/dudleystorey/pen/yJQxLX