Zoom in on a point (using scale and translate)
The better solution is to simply move the position of the viewport based on the change in the zoom. The zoom point is simply the point in the old zoom and the new zoom that you want to remain the same. Which is to say the viewport pre-zoomed and the viewport post-zoomed have the same zoompoint relative to the viewport. Given that we're scaling relative to the origin. You can adjust the viewport position accordingly:
scalechange = newscale - oldscale;offsetX = -(zoomPointX * scalechange);offsetY = -(zoomPointY * scalechange);
So really you can just pan over down and to the right when you zoom in, by a factor of how much you zoomed in, relative to the point you zoomed at.
Finally solved it:
const zoomIntensity = 0.2;const canvas = document.getElementById("canvas");let context = canvas.getContext("2d");const width = 600;const height = 200;let scale = 1;let originx = 0;let originy = 0;let visibleWidth = width;let visibleHeight = height;function draw(){ // Clear screen to white. context.fillStyle = "white"; context.fillRect(originx, originy, width/scale, height/scale); // Draw the black square. context.fillStyle = "black"; context.fillRect(50, 50, 100, 100); // Schedule the redraw for the next display refresh. window.requestAnimationFrame(draw);}// Begin the animation loop.draw();canvas.onwheel = function (event){ event.preventDefault(); // Get mouse offset. const mousex = event.clientX - canvas.offsetLeft; const mousey = event.clientY - canvas.offsetTop; // Normalize mouse wheel movement to +1 or -1 to avoid unusual jumps. const wheel = event.deltaY < 0 ? 1 : -1; // Compute zoom factor. const zoom = Math.exp(wheel * zoomIntensity); // Translate so the visible origin is at the context's origin. context.translate(originx, originy); // Compute the new visible origin. Originally the mouse is at a // distance mouse/scale from the corner, we want the point under // the mouse to remain in the same place after the zoom, but this // is at mouse/new_scale away from the corner. Therefore we need to // shift the origin (coordinates of the corner) to account for this. originx -= mousex/(scale*zoom) - mousex/scale; originy -= mousey/(scale*zoom) - mousey/scale; // Scale it (centered around the origin due to the trasnslate above). context.scale(zoom, zoom); // Offset the visible origin to it's proper position. context.translate(-originx, -originy); // Update scale and others. scale *= zoom; visibleWidth = width / scale; visibleHeight = height / scale;}
<canvas id="canvas" width="600" height="200"></canvas>
The key, as @Tatarize pointed out, is to compute the axis position such that the zoom point (mouse pointer) remains in the same place after the zoom.
Originally the mouse is at a distance mouse/scale
from the corner, we want the point under the mouse to remain in the same place after the zoom, but this is at mouse/new_scale
away from the corner. Therefore we need to shift the origin
(coordinates of the corner) to account for this.
originx -= mousex/(scale*zoom) - mousex/scale;originy -= mousey/(scale*zoom) - mousey/scale;scale *= zoom
The remaining code then needs to apply the scaling and translate to the draw context so it's origin coincides with the canvas corner.
This is actually a very difficult problem (mathematically), and I'm working on the same thing almost. I asked a similar question on Stackoverflow but got no response, but posted in DocType (StackOverflow for HTML/CSS) and got a response. Check it out http://doctype.com/javascript-image-zoom-css3-transforms-calculate-origin-example
I'm in the middle of building a jQuery plugin that does this (Google Maps style zoom using CSS3 Transforms). I've got the zoom to mouse cursor bit working fine, still trying to figure out how to allow the user to drag the canvas around like you can do in Google Maps. When I get it working I'll post code here, but check out above link for the mouse-zoom-to-point part.
I didn't realise there was scale and translate methods on Canvas context, you can achieve the same thing using CSS3 eg. using jQuery:
$('div.canvasContainer > canvas') .css('-moz-transform', 'scale(1) translate(0px, 0px)') .css('-webkit-transform', 'scale(1) translate(0px, 0px)') .css('-o-transform', 'scale(1) translate(0px, 0px)') .css('transform', 'scale(1) translate(0px, 0px)');
Make sure you set the CSS3 transform-origin to 0, 0 (-moz-transform-origin: 0 0). Using CSS3 transform allows you to zoom in on anything, just make sure the container DIV is set to overflow: hidden to stop the zoomed edges spilling out of the sides.
Whether you use CSS3 transforms, or canvas' own scale and translate methods is upto you, but check the above link for the calculations.
Update: Meh! I'll just post the code here rather than get you to follow a link:
$(document).ready(function(){ var scale = 1; // scale of the image var xLast = 0; // last x location on the screen var yLast = 0; // last y location on the screen var xImage = 0; // last x location on the image var yImage = 0; // last y location on the image // if mousewheel is moved $("#mosaicContainer").mousewheel(function(e, delta) { // find current location on screen var xScreen = e.pageX - $(this).offset().left; var yScreen = e.pageY - $(this).offset().top; // find current location on the image at the current scale xImage = xImage + ((xScreen - xLast) / scale); yImage = yImage + ((yScreen - yLast) / scale); // determine the new scale if (delta > 0) { scale *= 2; } else { scale /= 2; } scale = scale < 1 ? 1 : (scale > 64 ? 64 : scale); // determine the location on the screen at the new scale var xNew = (xScreen - xImage) / scale; var yNew = (yScreen - yImage) / scale; // save the current screen location xLast = xScreen; yLast = yScreen; // redraw $(this).find('div').css('-moz-transform', 'scale(' + scale + ')' + 'translate(' + xNew + 'px, ' + yNew + 'px' + ')') .css('-moz-transform-origin', xImage + 'px ' + yImage + 'px') return false; });});
You will of course need to adapt it to use the canvas scale and translate methods.
Update 2: Just noticed I'm using transform-origin together with translate. I've managed to implement a version that just uses scale and translate on their own, check it out here http://www.dominicpettifer.co.uk/Files/Mosaic/MosaicTest.html Wait for the images to download then use your mouse wheel to zoom, also supports panning by dragging the image around. It's using CSS3 Transforms but you should be able to use the same calculations for your Canvas.