How to implement a pinch-to-zoom image viewer in javascript.
By: Michael Alvarez
Tags: Canvas, JavaScript, Web Development, Image, Front End Development

Do you want to build an image viewer? One that works on desktop and mobile?

If so, this tutorial is for you! It was surprisingly difficult the first time I tried using canvas, so I hope this helps demystify canvas a little bit for you. It’s something I wish I had when the designer first asked me to create a mobile-friendly image viewer.

Demo & full code:

https://stackblitz.com/edit/js-image-viewer-article?file=index.js

Step 1: Add the HTML5 canvas drawing surface.

Note: For performance reasons, I chose to use HTML canvas, and NOT something like a background image & background-position. HTML Canvas was made for drawing whereas the DOM is going to be a lot slower for drawing/moving images.

Go to your index.html page and add the following HTML:

<canvas id="canvas" width="320" height="240"></canvas>

Step 2: In Javascript, get the canvas element and the canvas drawing context.

In your index.js file, add the following code:

document.onreadystatechange = (state) => { this.canvasEl = document.getElementById('canvas');

this.context = canvasEl.getContext('2d'); }

Step 3: Load the image

Before we can display the image, we need to do one thing. Load it!

In your index.js file, update the following code from step 2 with the new method loadImage

document.onreadystatechange = (state) => {

this.context = canvasEl.getContext('2d');

this.loadImage(` this.canvasEl = document.getElementById('canvas');this.context = canvasEl.getContext('2d');this.loadImage(` https://loremflickr.com/cache/resized/65535_49953967751_3cd51369d4_320_240_nofilter.jpg `); // TODO: Draw image after loading it } loadImage(src) { const image = new Image(); image.src = src; return new Promise((resolve) => { image.onload = () => { resolve(image); } }) }

Step 4: Draw the image on the canvas

In the .then callback we will draw our loadedImage onto the canvas. To do this, we need to use the context.drawImage method.

document.onreadystatechange = (state) => { this.canvasEl = document.getElementById('canvas');

this.context = canvasEl.getContext('2d');

this.loadImage(

`https://loremflickr.com/cache/resized/6101_6375301021_ba6610416d_n_320_240_nofilter.jpg`).then((loadedImage) => { this.context.drawImage(loadedImage, 0, 0); }); }



Step 6: Pan an image

To pan an image, we first need event listeners to listen for when the user clicks and drags on the image.

on mouse down, set isDragging to true on mouse up, when the user stops holding down their mouse, set isDragging = false. on mouse move, we will calculate the distance moved and then move the image that same amount.

In your index.js file, add a new method called setupListeners.

document.onreadystatechange = (state) => { this.canvasEl = document.getElementById('canvas');

this.context = canvasEl.getContext('2d');

this.loadImage(

`https://loremflickr.com/cache/resized/6101_6375301021_ba6610416d_n_320_240_nofilter.jpg`).then((loadedImage) => { this.context.drawImage(loadedImage, 0, 0);

setupListeners(); }); }

setupListeners() { this.canvasEl.addEventListener('mousedown', (e: MouseEvent) => handleMouseDown(e)); this.canvasEl.addEventListener('mouseup', () => this.handleMouseUp()); this.canvasEl.addEventListener('mousemove', (e: MouseEvent) => handleMouseMove(e)); this.canvasEl.addEventListener('mousewheel', (e: MouseEvent) => handleMouseWheel(e)); }

Step 7: Add handleMouseDown and handleMouseUp methods

The handleMouseDown method will be used to set isDragging to true, and also to keep track of the mouse position (x,y coordinates) when the user starts to drag. Similarly, handleMouseUp will be used to keep track of when dragging has stopped.

handleMouseDown(event) { this.mousedown = true; this.startPanX = event.clientX; this.startPanY = event.clientY; } handleMouseUp() { this.mousedown = false; this.isDragging = false; }

Step 8: Add handleMouseMove

This method calculates the distance travelled and is responsible for calling the redraw method to move the image for the panning behaviour.

Note: the slowMovementFactor is completely optional. It’s just used to slow the movement a little bit so that the image panning doesn’t move too quickly. Feel free to remove it.

handleMouseMove(event: MouseEvent) { if (!this.mousedown) { return; } const slowMovementFactor = 2; const deltaX = (this.startPanX - event.clientX); const deltaY = (this.startPanY - event.clientY); this.panX += deltaX / slowMovementFactor; this.panY += deltaY / slowMovementFactor; this.startPanX = event.clientX; this.startPanY = event.clientY; this.redraw(this.image, this.zoom); }

Step 9: Redraw the image when the user pans

We will first clearRect to make sure the canvas is completely cleaned off. To move the image, we will pass this.panX as the sx argument and this.panY as the sy to the context.drawImage() method.

redraw(image = this.image, zoom: number = 1) { if (!this.context) { return; } this.context.clearRect(0, 0, this.canvasEl.width, this.canvasEl.height); // Clear canvas const perceivedWidth = this.image.width * zoom; const perceivedHeight = this.image.height * zoom; let dx = 0; let dy = 0;

if (this.zoom > 1) { dx = -perceivedWidth / 4; dy = -perceivedHeight / 4; } else { dx = 0; dy = 0; } this.boundPan(); // See full code on stackblitz this.context.drawImage(image, this.panX, this.panY, image.width, image.height, dx, dy, perceivedWidth, perceivedHeight); }

Full Code

https://stackblitz.com/edit/js-image-viewer-article?file=index.html