Pure JavaScript Zoom and Pan, taken from http://phrogz.net/tmp/canvas_zoom_to_cursor.html
A Pen by TechSlides on CodePen.
Pure JavaScript Zoom and Pan, taken from http://phrogz.net/tmp/canvas_zoom_to_cursor.html
A Pen by TechSlides on CodePen.
<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 } |
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?
Is it possible to load an html page instead of an image?
I can't find any additional data on ctx.transformedPoint
on the spec. Is this a utility method you added to the prototype?
@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.
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?
I also want to control the zoom in and out. Is there a way out ?
please
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...
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.
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?
Hi @dzhang123, is there a way to limit the amount of zoom ? Because zooming in / out too much causes the image to disappear.