在动态HTML5画布中进行缩放和平移

5

我有一张地图。我希望用户能够缩放和平移地图。想象一下Google Maps,但是这张地图是一个正方形(如果你超出边缘,它不会再次包装)。

我已经使用scale()translate()实现了缩放和平移。这些都很好用。

我卡在了最后一步——当用户缩放时,我希望以该点为中心进行缩放。很难用语言来解释,所以想象一下在Google Maps中使用鼠标滚轮时发生的情况——这就是我想要的。

我已经查看了SO上所有标题中包含任何这些术语的答案。其中大多数都是关于这个问题的变体,基本上它们都说我需要做到这一点:

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 */);

然而,无论我做什么,似乎都不能使其正常工作。我可以让它在缩放后缩放到特定点,但无论我怎么做,我都无法使该点等于鼠标指针的位置。

请查看此示例。想象一下正方形是一张地图,圆圈是国家或其他东西。

我发现的最佳实现是此 SO 答案和链接的示例。然而,该代码使用了 SVG 和 .createSVGMatrix() 以及各种我无法理解的东西。如果可能的话,我希望能有一个完全基于 canvas 的解决方案。

显然,我对使用库并不感兴趣。我想要理解为什么我的方法无法正常工作。


1
我最近做了类似的事情,希望这对你有帮助。 https://dev59.com/d3rZa4cB1Zd3GeqP9PRv(没有你的代码,我认为这可能是“反向转换”中的数学错误(你也必须缩放该点))。 - Anti Earth
1
哦,这看起来很有趣!谢谢,我现在会去看看它。真不敢相信我错过了那个问题。我想我不应该只搜索“画布”解决方案。 - David John Welsh
1个回答

6

这里有一种在特定点缩放的技术:

绘制地图

简化绘制地图的过程,不使用变换来绘制地图(不需要translate、scale!)。

只需要使用context.drawImage的缩放版本即可。

你需要做的是将原始地图按照所需尺寸进行缩放,然后从用户选择的缩放点向上和向左拉动它。

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

选择缩放点(焦点):

缩放焦点实际上是2个点!

第一个焦点是鼠标点击的位置(mouseX,mouseY),用户单击以设置他们所需的缩放点。记住鼠标坐标在缩放空间中非常重要。用户看到/点击的地图是经过缩放的,因此他们的mouseX,mouseY也被缩放了。

第二个焦点是通过将鼠标坐标还原为未缩放的状态计算得出的。这个第二个点是原始未缩放地图上相当于鼠标位置的点。

使用第二个未缩放的焦点来计算从第一个焦点向左和向上拉动缩放地图的程度。

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);
}

缩放地图

当用户想要将地图放大或缩小时:

  • 计算新的缩放后地图的宽度和高度
  • 计算需要多少偏移量来从缩放点 (之前由鼠标位置选择) 向上和向左拉新缩放的地图。

代码:

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();
}

以下是示例代码和演示:

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>


这是一种相当不错的方法,但我认为它不能适用于我的情况。我的地图不是一个图像,而是使用画布动态绘制的(其中有一些动画效果,所以这是不可协商的)。回想起来,也许“像谷歌地图”这件事有点误导人,因为我没有使用图像 :-/ - David John Welsh
这种方法适用于动态绘制的画布。只需创建第二个内存画布,用于动态绘制地图。然后,您可以使用drawImage(myDynamicCanvas...而不是drawImage(image...。这样做的原因是第二个动态画布可以作为可见画布上drawImage的图像源。祝您的项目顺利! - markE
哦...那真是个好主意!从没想过用第二个画布。我会试一试的! - David John Welsh
这似乎对于最终只是一个简单的数学过程来说并不必要。在我概述的方法中,有什么让你感到困惑的吗? - Anti Earth
@AntiEarth 一点也不!至少我觉得不是这样。到目前为止,我只是简单地看了一下两种方法;我还没有时间去实现任何东西。现在终于有时间来处理这个问题了,所以我要更详细地了解一下,看看哪种方法更适合我的问题。我的实际项目比我做的测试更复杂——其中包括旋转——所以我还不确定任何事情。请放心,我会考虑所有的因素。 - David John Welsh

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接