Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save dzhang123/2a3a611b3d75a45a3f41 to your computer and use it in GitHub Desktop.
Save dzhang123/2a3a611b3d75a45a3f41 to your computer and use it in GitHub Desktop.
HTML5 Canvas Zoom and Pan Image
<p>Showing how to use transform methods on the HTML5 Canvas Context to selectively zoom in and out. Drag to pan. Click to zoom at that location. Shift-click to zoom out. Mousewheel up/down over the canvas to zoom in to/out from that location.</p>
<canvas></canvas>
var canvas = document.getElementsByTagName('canvas')[0];
canvas.width = 800;
canvas.height = 600;
var gkhead = new Image;
window.onload = function(){
var ctx = canvas.getContext('2d');
trackTransforms(ctx);
function redraw(){
// Clear the entire canvas
var p1 = ctx.transformedPoint(0,0);
var p2 = ctx.transformedPoint(canvas.width,canvas.height);
ctx.clearRect(p1.x,p1.y,p2.x-p1.x,p2.y-p1.y);
ctx.save();
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.restore();
ctx.drawImage(gkhead,0,0);
}
redraw();
var lastX=canvas.width/2, lastY=canvas.height/2;
var dragStart,dragged;
canvas.addEventListener('mousedown',function(evt){
document.body.style.mozUserSelect = document.body.style.webkitUserSelect = document.body.style.userSelect = 'none';
lastX = evt.offsetX || (evt.pageX - canvas.offsetLeft);
lastY = evt.offsetY || (evt.pageY - canvas.offsetTop);
dragStart = ctx.transformedPoint(lastX,lastY);
dragged = false;
},false);
canvas.addEventListener('mousemove',function(evt){
lastX = evt.offsetX || (evt.pageX - canvas.offsetLeft);
lastY = evt.offsetY || (evt.pageY - canvas.offsetTop);
dragged = true;
if (dragStart){
var pt = ctx.transformedPoint(lastX,lastY);
ctx.translate(pt.x-dragStart.x,pt.y-dragStart.y);
redraw();
}
},false);
canvas.addEventListener('mouseup',function(evt){
dragStart = null;
if (!dragged) zoom(evt.shiftKey ? -1 : 1 );
},false);
var scaleFactor = 1.1;
var zoom = function(clicks){
var pt = ctx.transformedPoint(lastX,lastY);
ctx.translate(pt.x,pt.y);
var factor = Math.pow(scaleFactor,clicks);
ctx.scale(factor,factor);
ctx.translate(-pt.x,-pt.y);
redraw();
}
var handleScroll = function(evt){
var delta = evt.wheelDelta ? evt.wheelDelta/40 : evt.detail ? -evt.detail : 0;
if (delta) zoom(delta);
return evt.preventDefault() && false;
};
canvas.addEventListener('DOMMouseScroll',handleScroll,false);
canvas.addEventListener('mousewheel',handleScroll,false);
};
gkhead.src = 'http://phrogz.net/tmp/gkhead.jpg';
// Adds ctx.getTransform() - returns an SVGMatrix
// Adds ctx.transformedPoint(x,y) - returns an SVGPoint
function trackTransforms(ctx){
var svg = document.createElementNS("http://www.w3.org/2000/svg",'svg');
var xform = svg.createSVGMatrix();
ctx.getTransform = function(){ return xform; };
var savedTransforms = [];
var save = ctx.save;
ctx.save = function(){
savedTransforms.push(xform.translate(0,0));
return save.call(ctx);
};
var restore = ctx.restore;
ctx.restore = function(){
xform = savedTransforms.pop();
return restore.call(ctx);
};
var scale = ctx.scale;
ctx.scale = function(sx,sy){
xform = xform.scaleNonUniform(sx,sy);
return scale.call(ctx,sx,sy);
};
var rotate = ctx.rotate;
ctx.rotate = function(radians){
xform = xform.rotate(radians*180/Math.PI);
return rotate.call(ctx,radians);
};
var translate = ctx.translate;
ctx.translate = function(dx,dy){
xform = xform.translate(dx,dy);
return translate.call(ctx,dx,dy);
};
var transform = ctx.transform;
ctx.transform = function(a,b,c,d,e,f){
var m2 = svg.createSVGMatrix();
m2.a=a; m2.b=b; m2.c=c; m2.d=d; m2.e=e; m2.f=f;
xform = xform.multiply(m2);
return transform.call(ctx,a,b,c,d,e,f);
};
var setTransform = ctx.setTransform;
ctx.setTransform = function(a,b,c,d,e,f){
xform.a = a;
xform.b = b;
xform.c = c;
xform.d = d;
xform.e = e;
xform.f = f;
return setTransform.call(ctx,a,b,c,d,e,f);
};
var pt = svg.createSVGPoint();
ctx.transformedPoint = function(x,y){
pt.x=x; pt.y=y;
return pt.matrixTransform(xform.inverse());
}
}
body { background:#eee; margin:1em; text-align:center; }
canvas { display:block; margin:1em auto; background:#fff; border:1px solid #ccc }
@dsathyakumar
Copy link

Hi @dzhang123, is there a way to limit the amount of zoom ? Because zooming in / out too much causes the image to disappear.

@jmevalentin
Copy link

Thanks for the great code. I second the concerns of zooming too much. I also want to get an xy coordinate value when user clicks the image. I have seen this done when using context.scale client coordinates and offset values to manage actual positions, but I don't know for sure how to implement that here. Any suggestions?

@colab0r
Copy link

colab0r commented Dec 8, 2017

Is it possible to load an html page instead of an image?

@giles-v
Copy link

giles-v commented Jan 17, 2018

I can't find any additional data on ctx.transformedPoint on the spec. Is this a utility method you added to the prototype?

@KonradLinkowski
Copy link

@giles-v
Line 81 // Adds ctx.getTransform() - returns an SVGMatrix
Line 86 ctx.getTransform = function(){ return xform; };
It's not added to prototype, it's added to an object. So inheritance is not affected.

@jadhavrajashri
Copy link

I'm working on an artificial intelligence application which draws shapes on images in the canvas. The shapes are drawn by the user.

I implemented zooming, using this above link : https://gist.github.com/dzhang123/2a3a611b3d75a45a3f41

However, once the image is zoomed, when user continues to draw, the lines appear with offset. The question is how to translate the

points so the lines to be drawn under the cursor again?

@jadhavrajashri
Copy link

I also want to control the zoom in and out. Is there a way out ?

@AkashPrsd
Copy link

please

@pedroheck
Copy link

Hey! Thank you so much for providing this code, it worked perfecly for my application! I was just wondering if there is a way to limit the zoom in/out? I can't seem to figure out how to do it...

@maxiptah
Copy link

maxiptah commented Nov 2, 2021

In the zoom function clicks are passed which basically define the zoom level. You could count clicks in the global variable and prevent further zooming, e.g.:

var curZoomIn = 0;
 var zoom = function (clicks) {

            var newZoomIn = curZoomIn + clicks;
            if (newZoomIn < 0 || newZoomIn > 5) {
                return;
            }

            curZoomIn += clicks;
...
}

This would limit zooming between the original size and 5x.

@stingermissile
Copy link

stingermissile commented Mar 10, 2023

This is awesome code. I have modified the zoom function to allow a minimum and maximum zoom level (also I have an array of layered canvas objects that I need to zoom in and out of):

let layer = [];
layer[0] = document.getElementById("canvas-layer-001");
layer[1] = document.getElementById("canvas-layer-002");
layer[2] = document.getElementById("canvas-layer-top");

let context = [];
context[0] = layer[0].getContext("2d");
context[1] = layer[1].getContext("2d");
context[2] = layer[2].getContext("2d");

const MIN_ZOOM_LEVEL = 0;
const MAX_ZOOM_LEVEL = 10;

let scaleFactor = 1.1;
let accumClicks = 0;

let zoom = function(clicks){
	if( accumClicks + clicks < MIN_ZOOM_LEVEL ||
		accumClicks + clicks > MAX_ZOOM_LEVEL ) { return; }

	let pt = context[i].transformedPoint(lastX,lastY);
	let factor = Math.pow(scaleFactor,clicks);

	for( let i = 0 ; i < context.length ; ++i ) {
		context[i].translate(pt.x,pt.y);
		context[i].scale(factor,factor);
		context[i].translate(-pt.x,-pt.y);
	}

	accumClicks += clicks;
	draw();
}

let resetZoom = function() {
	for( let i = 0 ; i < context.length ; ++i ){
		context[i].setTransform(1, 0, 0, 1, 0, 0);
	}
}

Issue I have is I do not want the zoom out to show white space (my application shows an image that takes up the entire canvas). Does anyone know how to do this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment