How to create a digital magnifying glass effect
Alan Stonebridge reveals how a combination of HTML5, CSS and JavaScript created net magazine's latest digital cover image.
For the cover of net magazine issue 252, we wanted add an extra element that is only possible in a digital edition - the ability to pick up the magnifying glass, move it around the cover, and for it behave like its real-life counterpart would.
Websites have used this sort of technique for years - the rollover effect on eBay listenings is just one example - but it's remarkably easy to replicate with an HTML5 canvas and small amounts of CSS and JavaScript.
Implementing this effect for net magazine's digital edition was simplified because we didn't have to worry about a diverse selection of web browsers. Our only concern was versions of the WebKit rendering engine used in recent versions of iOS.
However, for the explanation of how to reproduce this effect in your own websites, we've tested the following technique in the latest Chrome, Opera, Firefox, Internet Explorer 11, and desktop and mobile versions of Safari. The technique will require some adaptation to your scenario, but it demonstrates some basics of drawing bitmaps onto an HTML5 canvas.
Prerequisites
You'll need three images for this effect:
- A 768x1004-pixel JPEG image. A smaller image can be used, but you’ll need to change some of the numbers later in this tutorial to adapt it to your artwork.
- A larger JPEG of the same image. The dimensions of ours are 1536x2008 pixels. The size of this image affects the magnification of the lens. In conjunction with the dimensions of your smaller image, you’ll need to experiment to get a magnification that suits your purposes.
- An image of a magnifying glass as a PNG, ideally with semi-opaque pixels on its lens area to imitate the curvature and reflection of light on the glass.
Laying the groundwork
Here's the entirety of our HTML document that ties together the images and the other things we're yet to create. Our effect depends on three things in this document:
- The two img elements, which load the large, magnified image that appears in the lens, and the magnifying glass itself. Although referenced in our document's body, these images won't be displayed in it. We'll hide them from our style sheet.
- A canvas on which we'll detect the position of the mouse pointer or a finger, and draw the effect relative to that position.
- An onload event handler that calls the initialisation function in our JavaScript file.
The headline tags and div element are only here to add something extra to the page. This is helpful to ensure our effect works wherever the canvas is positioned on the page. We've also created links to an external stylesheet and our JavaScript.
Get the Creative Bloq Newsletter
Daily design news, reviews, how-tos and more, as picked by the editors.
<!DOCTYPE html>
<head>
<meta name="viewport">
<title>net magazine – issue 252 cover with magnifying glass effect</title>
<link rel="stylesheet" type="text/css" href="magnify.css">
<script src="magnify.js" type="text/javascript"></script>
</head>
<body onload="init()">
<img id="largeImage" src="./cover-large.jpg">
<img id="glassGraphic" src=“./cover-glass.png">
<h1>Magnifying glass effect</h1>
<h2>As seen on the cover of net magazine</h2>
<div id="description">
<p>How to recreate the magnification effect from the iPad edition of net magazine issue 252.</p>
</div>
<canvas id="canvas" width="768" height="1004"></canvas>
</body>
</html>
Styling things
Below is the content of our CSS file. Only the selectors targeting #canvas, #largeImage and #glassGraphic are used by our magnification effect. The others are for the extraneous elements in our HTML document.
All we need to do with the #largeImage and #glassGraphic elements is ensure they aren’t displayed in our document’s body. Our JavaScript uses their unique IDs to draw copies onto the canvas relative to the pointer or finger's position.
The selector for canvas sets our cover image as the background of the canvas and some display options. We've also set two WebKit-specific properties to suppress unwanted interactions with the canvas on iOS devices.
#canvas {
background: url('./cover.jpg');
background-size: cover;
background-repeat: no-repeat;
-webkit-touch-callout: none;
-webkit-user-select: none;
}
#largeImage, #glassGraphic { display: none; }
body {
margin: 16px 20px;
padding: 0;
background: rgb(224,224,224);
}
h1 { font-size: 24pt; margin: 0; }
h2 { font-size: 20pt; margin-top: 0; margin-bottom: 24px; }
#description {
background: rgb(192,192,192);
border-radius: 10px;
padding: 8px 16px;
width: 140px;
height: auto;
float: left;
margin-right: 40px;
}
Building the magnification effect
The work of drawing the magnifying glass is done in JavaScript. First, we need to create some global constants and variables to keep track of a few things. Add these definitions to the top of your JavaScript file.
const lensRadius = 220;
const grabPointOffsetX = 175;
const grabPointOffsetY = 175;
var zoom;
var canvas, context;
var mouseX, mouseXConstrained, mouseY, mouseYConstrained;
var xSource, ySource;
var xMinimum, xMaximum, yMinimum, yMaximum;
We'll explain these variables as we make use of them. Below those definitions, create the init() function that's called when the HTML document loads:
function init() {
canvas = document.getElementById("canvas");
canvas.addEventListener("mousemove", mouseTrack, false);
canvas.addEventListener("touchmove", touchTrack, true);
context = canvas.getContext("2d");
xMinimum = canvas.offsetLeft + 198 - grabPointOffsetX;
xMaximum = canvas.offsetLeft + 570 - grabPointOffsetX;
yMinimum = canvas.offsetTop + 198 + grabPointOffsetY;
yMaximum = canvas.offsetTop + 806 + grabPointOffsetY;
mouseX = 221 + canvas.offsetLeft;
mouseY = 756 + canvas.offsetTop;
zoom = document.getElementById("largeImage").width / canvas.width * 0.9;
drawMagGlass();
}
First, this function stores a reference to the canvas, and binds event listeners that check for mouse and touch-based movement to mouseTrack and touchTrack functions, which we'll write in a moment.
After that, we've assigned values to four of our global variables. These values take into account the position of the canvas's top-left corner in the HTML document, so that the effect will work wherever that happens to be. Each of these assignments contains a different hard-coded value. If you've already tried out this effect on our digital edition, you may have noticed you can only move the magnifying glass so far towards the cover's edges.
We deliberately constrained the range of its movement because moving it further attempts to read pixel data that doesn't exist to fill the lens. This made sense in the context of our digital magazine, but it means that the magnifying glass will be cropped where it overruns the edge of the canvas in a web page. You will need to experiment with the hard-coded values to get a range of movement that suits your purposes.
These assignments also take into account a horizontal or a vertical offset. We've used these global constants here and in other calculations so that the lens area of our magnifying glass doesn't appear under the pointer or finger. Instead, it will seem like you're holding the magnifying glass around the top of its handle.
Last of all, we set default coordinates at which to draw the magnifying glass, created a zoom level that gives us an aesthetically pleasing multiplier from which to pull pixels from the larger image, and draw the magnifying glass with a call to drawMagGlass(). Let's write that function now, in short chunks from start to finish:
function drawMagGlass() {
constrainPosition();
xSource = 2*mouseXConstrained - zoom * lensRadius + 2*grabPointOffsetX;
ySource = 2*mouseYConstrained - zoom * lensRadius - 2*grabPointOffsetY;
The first statement relates to the scenario of preventing the magnifying glass moving too far outwards in any direction. We’ll define that function after this one, but we’ve added the call to it now because subsequent calculations depend on it. The second and third statements use the constrained coordinates to work out where in the larger, magnified image to pull pixel data to fill the lens.
context.clearRect(0, 0, canvas.width, canvas.height);
Next we wipe anything we've previously drawn on the canvas. There's nothing to clear prior to drawMagGlass() being called by our initialisation function, but we'll also call this function whenever the position of the pointer or finger changes.
Without this call, previous drawings of the magnifying glass would persist and create a trail across the canvas. Note that this function doesn't wipe the background image of the canvas - our issue cover - that is set by our stylesheet.
context.save();
Right now, our drawing context covers the entire canvas, but we want to constrain it to a circle that represents the magnifying glass's lens. After we've drawn the lens' contents, we'll draw the magnifying glass over it. To be able to do that, we need to be able to return to drawing anywhere on the canvas. That's what this invocation of save() and a later call of restore() are about.
context.beginPath();
context.arc(mouseXConstrained+grabPointOffsetX, mouseYConstrained-grabPointOffsetY, lensRadius, 0, 2*Math.PI, false);
context.closePath();
context.clip();
These statements draw a path that forms a circle. As we mentioned earlier, it's drawn offset from the position of the pointer or finger. The circle's size is set according to the lensRadius global constant we defined earlier.
You'll need to tailor the radius and offset will depend on the magnifying glass graphic you use. After closing the path, the call of clip() ensures that subsequent drawing to the canvas is limited to the interior of the path.
Our next statement uses another built-in canvas method to draw the contents of the lens. We've used the constrained mouse position, the values calculated at the start of drawMagGlass() and several of our global constants to pull pixels from our large image.
context.drawImage(largeImage,
xSource,
ySource,
zoom*2*lensRadius,
zoom*2*lensRadius,
mouseXConstrained-lensRadius+grabPointOffsetX,
mouseYConstrained-lensRadius-grabPointOffsetY,
2*lensRadius,
2*lensRadius);
The nine parameters supplied here are:
- The source image, which is the larger version of our cover
- The x and y coordinates from the source image from which to start reading
- How many pixels to read, horizontally and vertically
- The x and y coordinates on the canvas at which to draw the pixels from the source image
- The width and height into which the pixels are drawn, which covers the entire lens area.
context.restore();
This statement restores the drawing context that we saved earlier, enabling us to draw anywhere on the canvas so the magnifying glass can be drawn on top of and around the lens.
context.drawImage(glassGraphic,
mouseXConstrained-556+grabPointOffsetX,
mouseYConstrained-230-grabPointOffsetY);
The drawImage() method can be invoked with fewer parameters. Only three are needed to draw our magnifying glass, which we've already preloaded but hidden in our HTML document's body. We reference that image by the name set in its id attribute, and specify the coordinates at which to draw it.
Those coordinates take into account our predefined offset to make it appear we're holding the glass at the top of its handle. The image we've used contains semi-opaque pixels to imitate a reflection on the lens’ convex glass. That's all the drawing we need to do, so we close our function's body.
Filling in the remaining gaps
Earlier, we said we'd define the constrainPosition() function, which restricts the movement of magnifying glass to a sensible region of our image, so that the contents of the lens never disappear as a consequence of trying to read data from beyond the boundaries of our larger image.
This function consists of two simple conditional statements that ensure the x and y coordinates remain within acceptable ranges. You'll need to experiment based on the images you're using with this effect and the context in which it appears.
function constrainPosition() {
if (mouseX < xMinimum) {
mouseXConstrained = xMinimum - canvas.offsetLeft;
} else if (mouseX > xMaximum) {
mouseXConstrained = xMaximum - canvas.offsetLeft;
} else {
mouseXConstrained = mouseX - canvas.offsetLeft;
}
if (mouseY < yMinimum) {
mouseYConstrained = yMinimum - canvas.offsetTop;
} else if (mouseY > yMaximum) {
mouseYConstrained = yMaximum - canvas.offsetTop;
} else {
mouseYConstrained = mouseY - canvas.offsetTop;
}
}
If the pointer or finger is too far to the left or the right, the constrained coordinate takes the corresponding xMinimum or xMaximum value that we set as a global constant. We take into account the left coordinate of the canvas’s position in this calculation.
This calculation is repeated for the y coordinate and the corresponding yMinimum and yMaximum values set earlier.
There last piece of the jigsaw is to track the position of the pointer or a finger on the canvas. We've already bound two functions - mouseTrack and touchTrack - to the mousemove and touchmove events, respectively. Each of these functions simply assigns the components of the coordinate to global variables, then calls drawMagGlass() to wipe the magnifying glass from the canvas and redraw it.
function mouseTrack(e) {
if (!e) { var e = event; }
mouseX = e.clientX + ((document.documentElement.scrollLeft) ? document.documentElement.scrollLeft : document.body.scrollLeft);
mouseY = e.clientY + ((document.documentElement.scrollTop) ? document.documentElement.scrollTop : document.body.scrollTop);
drawMagGlass();
}
function touchTrack(e) {
if (!e) { var e = event; }
mouseX = e.targetTouches[0].pageX;
mouseY = e.targetTouches[0].pageY;
drawMagGlass();
}
This implementation instantly redraws the magnifying glass at the coordinates. With touch-based input, the magnifying glass jumps to wherever you tap, but with your finger held against the screen you’ll get the illusion of dragging the magnifying glass. With a pointer-based input device, it will track the pointer's movement across the canvas without having to hold down a button.
Words: Alan Stonebridge
Explore the future of SEO in net magazine issue 252 - on sale now!
Thank you for reading 5 articles this month* Join now for unlimited access
Enjoy your first month for just £1 / $1 / €1
*Read 5 free articles per month without a subscription
Join now for unlimited access
Try first month for just £1 / $1 / €1
The Creative Bloq team is made up of a group of design fans, and has changed and evolved since Creative Bloq began back in 2012. The current website team consists of eight full-time members of staff: Editor Georgia Coggan, Deputy Editor Rosie Hilder, Ecommerce Editor Beren Neale, Senior News Editor Daniel Piper, Editor, Digital Art and 3D Ian Dean, Tech Reviews Editor Erlingur Einarsson and Ecommerce Writer Beth Nicholls and Staff Writer Natalie Fear, as well as a roster of freelancers from around the world. The 3D World and ImagineFX magazine teams also pitch in, ensuring that content from 3D World and ImagineFX is represented on Creative Bloq.