Zoom and pan in an animated HTML5 canvas

I have a map. I want the user to be able to zoom and pan the map. Imagine Google Maps, but instead of being infinitely movable, the map is a square (it doesn't wrap around again if you walk past its edge).

I used zoom and pan with scale()

and translate()

. They work well.

I'm stuck with the final part - when the user zooms in, I want to center the scale around that point. It's hard to explain in words, so just imagine what happens when you use the mouse wheel on Google Maps - that's what I want.

I've looked at every answer on SO with any of these terms in the title. Most of them are variations of this that basically say this is what I need to do:

ctx.translate(/* to the point where the mouse is */);
ctx.scale(/* to zoom level I want */)
ctx.translate(/* back to the point where the mouse was, taking zoom into account */);

      

However, whatever I do, I cannot get it to work. I can get it to zoom in to a certain point after scaling, but whatever I do, I cannot make that point equal to where the mouse pointer was.

Check this script . Imagine the square is a map and the circles are countries or whatever.

The best implementation I've found is this SO answer and related example . However, the code uses SVG and .createSVGMatrix()

and all sorts of things that honestly I can't figure out. I would prefer a fully-canvas-based solution if possible.

Obviously I'm not interested in doing this with a library. I want to understand why what I am doing is not working.

+3


source to share


1 answer


Here's one method for scaling to point:

Drawing a map

Simplify things by not using transforms to draw the map (no need to translate, scale!).

All it takes is a scalable version context.drawImage

.

What you do is scale the original map to the desired size and then drag it up and to the left from the zoom point the user has chosen.

context.drawImage(
    map,
    0,0,map.width,map.height,  // start with the map at original (unscaled) size
    offsetX,offsetY,           // pull the map leftward & upward from the scaling point
    scaledWidth,scaledHeight   // resize the map to the currently scaled size

      

Zoom point (focal point) selection:

The zoom focal point is actually 2 points!

The first focal point is mouseX, mouseY, where the user pressed to set the desired zoom point. It is important to remember that the mouse coordinate is in scaled space . The map the user sees / clicks is scaled, so mouseX, mouseY scales as well.

The second focal point is calculated by scaling the mouse coordinate. This second point is the equivalent mouse position on the original unscaled map.



The second unscaled focal point is used to calculate how much to stretch the scaled map left and up from the first focal point.

function setFocus(mx,my){
    // mouseX,mouseY is the scaling point in scaled coordinates
    focusX=mx;
    focusY=my;
    // convert the scaled focal point
    // to an unscaled focal point
    focusX1=parseInt((mx-mapLeft)/scale);
    focusY1=parseInt((my-mapTop)/scale);
}

      

Map scaling

When the user indicates that they want to scale the map more or less:

  • calculate the new scaled width and height of the map
  • calculate how many displacements are needed in order to pull the newly scaled map up and to the left of the zoom point (the zoom point was previously selected by the mouse position).

code:

function setScale(newScale){
    scale=newScale;
    // calc the width & height of the newly scaled map
    mapWidth=parseInt(iw*scale);
    mapHeight=parseInt(ih*scale);
    // calc how much to offset the map on the canvas
    mapLeft=parseInt(focusX-focusX1*scale);
    mapTop =parseInt(focusY-focusY1*scale);
    // draw the map
    drawMap();
}

      

Here's some sample code and demo example:

var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var $canvas=$("#canvas");
var canvasOffset=$canvas.offset();
var offsetX=canvasOffset.left;
var offsetY=canvasOffset.top;

//
var counter=1;
var PI2=Math.PI*2;
var iw,ih;
var mapLeft,mapTop,mapWidth,mapHeight;
var focusX,focusY,focusX1,focusY1;
var scale;

var map=new Image();
map.onload=start;
map.src="https://dl.dropboxusercontent.com/u/139992952/multple/mapSmall.png";
function start(){

  iw=map.width;
  ih=map.height;

  // initial 
  mapLeft=0;
  mapTop=0;
  scale=1.00;

  setFocus(iw/2*scale,ih/2*scale);

  setScale(scale);   // also sets mapWidth,mapHeight

  drawMap();

  //
  $("#canvas").mousedown(function(e){handleMouseDown(e);});

  //
  canvas.addEventListener('DOMMouseScroll',handleScroll,false);
  canvas.addEventListener('mousewheel',handleScroll,false);
}

//
function setScale(newScale){
  scale=newScale;
  mapWidth=parseInt(iw*scale);
  mapHeight=parseInt(ih*scale);    
  mapLeft=parseInt(focusX-focusX1*scale);
  mapTop =parseInt(focusY-focusY1*scale);
  drawMap();
}

//
function setFocus(mx,my){
  // mouseX,mouseY is the scaling point in scaled coordinates
  focusX=mx;
  focusY=my;
  // convert the scaled focal point
  // to an unscaled focal point
  focusX1=parseInt((mx-mapLeft)/scale);
  focusY1=parseInt((my-mapTop)/scale);
  //
  drawMap();
}

//
function drawMap(){
  ctx.clearRect(0,0,canvas.width,canvas.height);
  ctx.save();
  ctx.drawImage(map,0,0,iw,ih,mapLeft,mapTop,mapWidth,mapHeight);
  dot(ctx,focusX,focusY,"red");
  ctx.restore();
}

function dot(ctx,x,y,fill){
  ctx.beginPath();
  ctx.arc(x,y,4,0,PI2);
  ctx.closePath();
  ctx.fillStyle=fill;
  ctx.fill();
  ctx.lineWidth=2;
  ctx.stroke();
}

//
function handleScroll(e){
  e.preventDefault();
  e.stopPropagation();

  var delta=e.wheelDelta?e.wheelDelta/30:e.detail?-e.detail:0;
  if (delta){
    counter+=delta;
    setScale(1+counter/100);
  }
};

//
function handleMouseDown(e){
  e.preventDefault();
  e.stopPropagation();
  mouseX=parseInt(e.clientX-offsetX);
  mouseY=parseInt(e.clientY-offsetY);
  setFocus(mouseX,mouseY);
  drawMap();
}
      

body{ background-color: ivory; }
canvas{border:1px solid red;}
      

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4>Click to set zoom point<br>Use mousewheel to zoom</h4>
<canvas id="canvas" width=600 height=400></canvas><br>
      

Run codeHide result


+5


source







All Articles