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 }
@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