【问题标题】:Zoom in on a point (using scale and translate)放大一个点(使用缩放和平移)
【发布时间】:2011-02-24 08:33:41
【问题描述】:

我希望能够放大 HTML 5 画布中鼠标下方的点,例如放大 Google Maps。我怎样才能做到这一点?

【问题讨论】:

  • 我用它来缩放我的画布,效果很好!我唯一要补充的是,缩放量的计算与您期望的不同。 “var 缩放 = 1 + 轮子/2;”即这导致 1.5 放大和 0.5 缩小。我在我的版本中编辑了这个,所以我有 1.5 用于放大和 1/1.5 用于缩小,这使得放大和缩小的量相等。因此,如果您放大一次然后再缩小,您将获得与缩放前相同的图片。
  • 请注意,这不适用于 Firefox,但该方法可以轻松应用于 jQuery mousewheel plugin。感谢分享!
  • var zoom = Math.pow(1.5f, wheel); // 使用它来计算缩放。它的好处是通过wheel=2 缩放与通过wheel=1 缩放两次相同。另外,放大+2,缩小+2恢复原比例。

标签: javascript html canvas


【解决方案1】:

这实际上是一个非常困难的问题(数学上),我几乎也在做同样的事情。我在 Stackoverflow 上问了一个类似的问题,但没有得到回应,但在 DocType(HTML/CSS 的 StackOverflow)中发布并得到了回应。看看http://doctype.com/javascript-image-zoom-css3-transforms-calculate-origin-example

我正在构建一个执行此操作的 jQuery 插件(使用 CSS3 转换的 Google 地图样式缩放)。我的鼠标光标缩放位工作正常,仍在试图弄清楚如何允许用户像在谷歌地图中那样拖动画布。当我让它工作时,我会在这里发布代码,但请查看上面的链接以获取鼠标缩放到点部分。

我没有意识到 Canvas 上下文中有缩放和翻译方法,您可以使用 CSS3 来实现相同的效果,例如。使用 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)');

确保将 CSS3 transform-origin 设置为 0, 0 (-moz-transform-origin: 0 0)。使用 CSS3 转换可以放大任何东西,只需确保容器 DIV 设置为溢出:隐藏以阻止缩放的边缘溢出侧面。

您是否使用 CSS3 转换,或画布自己的缩放和转换方法取决于您,但请查看上面的链接进行计算。


更新: 呸!我只是在这里发布代码,而不是让你点击链接:

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

您当然需要调整它以使用画布比例和翻译方法。


更新 2: 刚刚注意到我正在使用 transform-origin 和 translate。我已经设法实现了一个只使用缩放和自己翻译的版本,在这里查看http://www.dominicpettifer.co.uk/Files/Mosaic/MosaicTest.html 等待图像下载然后使用鼠标滚轮进行缩放,还支持通过拖动图像进行平移。它使用 CSS3 转换,但您应该能够为您的画布使用相同的计算。

【讨论】:

  • 我终于解决了,在做了大约 2 周的其他事情后,我花了 3 分钟时间
  • @Synday Ironfoot 在他的更新上的链接不起作用。这个链接:dominicpettifer.co.uk/Files/Mosaic/MosaicTest.html我想要这个实施。你可以在这里发布代码吗?谢谢
  • 截至今天(2014 年 9 月),指向 MosaicTest.html 的链接已失效。
  • 马赛克演示已消失。我通常使用 vanilla js 而不是 jQuery。 $(this) 指的是什么? document.body.offsetTop?我真的很想看到我的foreverscape.com 项目真正受益的马赛克演示。
  • 马赛克演示页面保存在archive.org上:web.archive.org/web/20130126152008/http://…
【解决方案2】:

终于解决了:

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;
}
&lt;canvas id="canvas" width="600" height="200"&gt;&lt;/canvas&gt;

关键,如@Tatarize pointed out,是计算轴位置,使缩放点(鼠标指针)在缩放后保持在同一位置。

最初鼠标在距离角mouse/scale处,我们希望鼠标下的点在缩放后保持在同一位置,但这是在远离角的mouse/new_scale。因此我们需要移动origin(角的坐标)来解决这个问题。

originx -= mousex/(scale*zoom) - mousex/scale;
originy -= mousey/(scale*zoom) - mousey/scale;
scale *= zoom

剩下的代码需要应用缩放并转换到绘图上下文,使其原点与画布角重合。

【讨论】:

  • 谢谢老兄,在找到您的代码之前几乎迷路了 2 天
  • 嘿,我只是在寻找这样的东西,只是想说让你破解它!
  • 这如何应用于 dom 节点?
  • 清除画布时 800 和 600 值代表什么?
  • @GeorgianStan 那是我忘记更改的宽度和高度。现在用命名变量替换它们。
【解决方案3】:

我想在这里为那些单独绘制图片和移动缩放的人提供一些信息。

当您想要存储视口的缩放和位置时,这可能很有用。

这是抽屉:

function redraw_ctx(){
   self.ctx.clearRect(0,0,canvas_width, canvas_height)
   self.ctx.save()
   self.ctx.scale(self.data.zoom, self.data.zoom) // 
   self.ctx.translate(self.data.position.left, self.data.position.top) // position second
   // Here We draw useful scene My task - image:
   self.ctx.drawImage(self.img ,0,0) // position 0,0 - we already prepared
   self.ctx.restore(); // Restore!!!
}

注意比例必须是第一位的

这里是缩放器:

function zoom(zf, px, py){
    // zf - is a zoom factor, which in my case was one of (0.1, -0.1)
    // px, py coordinates - is point within canvas 
    // eg. px = evt.clientX - canvas.offset().left
    // py = evt.clientY - canvas.offset().top
    var z = self.data.zoom;
    var x = self.data.position.left;
    var y = self.data.position.top;

    var nz = z + zf; // getting new zoom
    var K = (z*z + z*zf) // putting some magic

    var nx = x - ( (px*zf) / K ); 
    var ny = y - ( (py*zf) / K);

    self.data.position.left = nx; // renew positions
    self.data.position.top = ny;   
    self.data.zoom = nz; // ... and zoom
    self.redraw_ctx(); // redraw context
    }

当然,我们还需要一个拖动器:

this.my_cont.mousemove(function(evt){
    if (is_drag){
        var cur_pos = {x: evt.clientX - off.left,
                       y: evt.clientY - off.top}
        var diff = {x: cur_pos.x - old_pos.x,
                    y: cur_pos.y - old_pos.y}

        self.data.position.left += (diff.x / self.data.zoom);  // we want to move the point of cursor strictly
        self.data.position.top += (diff.y / self.data.zoom);

        old_pos = cur_pos;
        self.redraw_ctx();

    }


})

【讨论】:

    【解决方案4】:

    我在使用 c++ 时遇到了这个问题,我可能不应该只使用 OpenGL 矩阵开始...无论如何,如果您使用的控件的原点是左上角,并且您想要像谷歌地图一样平移/缩放,这是布局(使用 allegro 作为我的事件处理程序):

    // initialize
    double originx = 0; // or whatever its base offset is
    double originy = 0; // or whatever its base offset is
    double zoom = 1;
    
    .
    .
    .
    
    main(){
    
        // ...set up your window with whatever
        //  tool you want, load resources, etc
    
        .
        .
        .
        while (running){
            /* Pan */
            /* Left button scrolls. */
            if (mouse == 1) {
                // get the translation (in window coordinates)
                double scroll_x = event.mouse.dx; // (x2-x1) 
                double scroll_y = event.mouse.dy; // (y2-y1) 
    
                // Translate the origin of the element (in window coordinates)      
                originx += scroll_x;
                originy += scroll_y;
            }
    
            /* Zoom */ 
            /* Mouse wheel zooms */
            if (event.mouse.dz!=0){    
                // Get the position of the mouse with respect to 
                //  the origin of the map (or image or whatever).
                // Let us call these the map coordinates
                double mouse_x = event.mouse.x - originx;
                double mouse_y = event.mouse.y - originy;
    
                lastzoom = zoom;
    
                // your zoom function 
                zoom += event.mouse.dz * 0.3 * zoom;
    
                // Get the position of the mouse
                // in map coordinates after scaling
                double newx = mouse_x * (zoom/lastzoom);
                double newy = mouse_y * (zoom/lastzoom);
    
                // reverse the translation caused by scaling
                originx += mouse_x - newx;
                originy += mouse_y - newy;
            }
        }
    }  
    
    .
    .
    .
    
    draw(originx,originy,zoom){
        // NOTE:The following is pseudocode
        //          the point is that this method applies so long as
        //          your object scales around its top-left corner
        //          when you multiply it by zoom without applying a translation.
    
        // draw your object by first scaling...
        object.width = object.width * zoom;
        object.height = object.height * zoom;
    
        //  then translating...
        object.X = originx;
        object.Y = originy; 
    }
    

    【讨论】:

      【解决方案5】:

      这是另一种方法,它使用 setTransform() 代替 scale() 和 translate()。一切都存储在同一个对象中。画布假定在页面上的 0,0,否则您需要从页面坐标中减去它的位置。

      this.zoomIn = function (pageX, pageY) {
          var zoomFactor = 1.1;
          this.scale = this.scale * zoomFactor;
          this.lastTranslation = {
              x: pageX - (pageX - this.lastTranslation.x) * zoomFactor,
              y: pageY - (pageY - this.lastTranslation.y) * zoomFactor
          };
          this.canvasContext.setTransform(this.scale, 0, 0, this.scale,
                                          this.lastTranslation.x,
                                          this.lastTranslation.y);
      };
      this.zoomOut = function (pageX, pageY) {
          var zoomFactor = 1.1;
          this.scale = this.scale / zoomFactor;
          this.lastTranslation = {
              x: pageX - (pageX - this.lastTranslation.x) / zoomFactor,
              y: pageY - (pageY - this.lastTranslation.y) / zoomFactor
          };
          this.canvasContext.setTransform(this.scale, 0, 0, this.scale,
                                          this.lastTranslation.x,
                                          this.lastTranslation.y);
      };
      

      伴随代码处理平移:

      this.startPan = function (pageX, pageY) {
          this.startTranslation = {
              x: pageX - this.lastTranslation.x,
              y: pageY - this.lastTranslation.y
          };
      };
      this.continuePan = function (pageX, pageY) {
          var newTranslation = {x: pageX - this.startTranslation.x,
                                y: pageY - this.startTranslation.y};
          this.canvasContext.setTransform(this.scale, 0, 0, this.scale,
                                          newTranslation.x, newTranslation.y);
      };
      this.endPan = function (pageX, pageY) {
          this.lastTranslation = {
              x: pageX - this.startTranslation.x,
              y: pageY - this.startTranslation.y
          };
      };
      

      要自己得出答案,请考虑相同的页面坐标需要在缩放前后匹配相同的画布坐标。然后你可以从这个方程开始做一些代数:

      (pageCoords - 翻译) / scale = canvasCoords

      【讨论】:

        【解决方案6】:
        if(wheel > 0) {
            this.scale *= 1.1; 
            this.offsetX -= (mouseX - this.offsetX) * (1.1 - 1);
            this.offsetY -= (mouseY - this.offsetY) * (1.1 - 1);
        }
        else {
            this.scale *= 1/1.1; 
            this.offsetX -= (mouseX - this.offsetX) * (1/1.1 - 1);
            this.offsetY -= (mouseY - this.offsetY) * (1/1.1 - 1);
        }
        

        【讨论】:

        • 参考mouseXmouseY 会很有帮助。
        【解决方案7】:

        更好的解决方案是简单地根据缩放的变化移动视口的位置。缩放点只是旧缩放和新缩放中要保持不变的点。也就是说,预缩放的视口和缩放后的视口相对于视口具有相同的缩放点。鉴于我们正在相对于原点进行缩放。您可以相应地调整视口位置:

        scalechange = newscale - oldscale;
        offsetX = -(zoomPointX * scalechange);
        offsetY = -(zoomPointY * scalechange);
        

        所以实际上你可以在放大时向下和向右平移,相对于你放大的点,你放大了多少。

        【讨论】:

        • 比剪切和粘贴代码更有价值的是解释最佳解决方案是什么以及为什么它可以在没有包袱的情况下工作,特别是如果它是三行长。
        • scalechange = newscale / oldscale?
        • 另外,我想为那些寻求实现像 pan-zoom 组件这样的地图添加,鼠标 X,Y 应该是 (mousePosRelativeToContainer - currentTransform)/currentScale 否则它将处理当前鼠标相对于容器的位置。
        • 是的,这个数学假设缩放和平移都在与原点相关的坐标中。如果它们与视口相关,则必须适当地调整它们。虽然我认为正确的数学是 zoomPoint = (mousePosRelativeToContainer + currentTranslation)。该数学还假设原点通常位于字段的左上角。但是,考虑到简单性,针对稍微不典型的情况进行调整要容易得多。
        • 这是一个不错的解决方案,但对我来说,我需要将结果偏移量除以 new_zoom。 var offsetX = -(mouse_offset_x * scalechange)/newzoom;
        【解决方案8】:

        你需要在缩放前后获取世界空间(相对于屏幕空间)的点,然后按增量平移。

        mouse_world_position = to_world_position(mouse_screen_position);
        zoom();
        mouse_world_position_new = to_world_position(mouse_screen_position);
        translation += mouse_world_position_new - mouse_world_position;
        

        鼠标位置在屏幕空间中,因此您必须将其转换为世界空间。 简单的变换应该是这样的:

        world_position = screen_position / scale - translation
        

        【讨论】:

          【解决方案9】:

          这是我针对中心图像的解决方案:

          var MIN_SCALE = 1;
          var MAX_SCALE = 5;
          var scale = MIN_SCALE;
          
          var offsetX = 0;
          var offsetY = 0;
          
          var $image     = $('#myImage');
          var $container = $('#container');
          
          var areaWidth  = $container.width();
          var areaHeight = $container.height();
          
          $container.on('wheel', function(event) {
              event.preventDefault();
              var clientX = event.originalEvent.pageX - $container.offset().left;
              var clientY = event.originalEvent.pageY - $container.offset().top;
          
              var nextScale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, scale - event.originalEvent.deltaY / 100));
          
              var percentXInCurrentBox = clientX / areaWidth;
              var percentYInCurrentBox = clientY / areaHeight;
          
              var currentBoxWidth  = areaWidth / scale;
              var currentBoxHeight = areaHeight / scale;
          
              var nextBoxWidth  = areaWidth / nextScale;
              var nextBoxHeight = areaHeight / nextScale;
          
              var deltaX = (nextBoxWidth - currentBoxWidth) * (percentXInCurrentBox - 0.5);
              var deltaY = (nextBoxHeight - currentBoxHeight) * (percentYInCurrentBox - 0.5);
          
              var nextOffsetX = offsetX - deltaX;
              var nextOffsetY = offsetY - deltaY;
          
              $image.css({
                  transform : 'scale(' + nextScale + ')',
                  left      : -1 * nextOffsetX * nextScale,
                  right     : nextOffsetX * nextScale,
                  top       : -1 * nextOffsetY * nextScale,
                  bottom    : nextOffsetY * nextScale
              });
          
              offsetX = nextOffsetX;
              offsetY = nextOffsetY;
              scale   = nextScale;
          });
          body {
              background-color: orange;
          }
          #container {
              margin: 30px;
              width: 500px;
              height: 500px;
              background-color: white;
              position: relative;
              overflow: hidden;
          }
          img {
              position: absolute;
              top: 0;
              bottom: 0;
              left: 0;
              right: 0;
              max-width: 100%;
              max-height: 100%;
              margin: auto;
          }
          <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
          
          <div id="container">
              <img id="myImage" src="https://via.placeholder.com/300">
          </div>

          【讨论】:

            【解决方案10】:

            您可以使用scrollto(x,y)函数将滚动条的位置处理到缩放后需要显示的位置。找到鼠标的位置使用event.clientX和event.clientY。 this will help you

            【讨论】:

              【解决方案11】:

              这是@tatarize 答案的代码实现,使用 PIXI.js。我有一个查看非常大图像的一部分的视口(例如谷歌地图样式)。

              $canvasContainer.on('wheel', function (ev) {
              
                  var scaleDelta = 0.02;
                  var currentScale = imageContainer.scale.x;
                  var nextScale = currentScale + scaleDelta;
              
                  var offsetX = -(mousePosOnImage.x * scaleDelta);
                  var offsetY = -(mousePosOnImage.y * scaleDelta);
              
                  imageContainer.position.x += offsetX;
                  imageContainer.position.y += offsetY;
              
                  imageContainer.scale.set(nextScale);
              
                  renderer.render(stage);
              });
              
              • $canvasContainer 是我的 html 容器。
              • imageContainer 是我的 PIXI 容器,里面有图片。
              • mousePosOnImage 是鼠标相对于整个图像(不仅仅是视口)的位置。

              这是我获得鼠标位置的方法:

                imageContainer.on('mousemove', _.bind(function(ev) {
                  mousePosOnImage = ev.data.getLocalPosition(imageContainer);
                  mousePosOnViewport.x = ev.data.originalEvent.offsetX;
                  mousePosOnViewport.y = ev.data.originalEvent.offsetY;
                },self));
              

              【讨论】:

                【解决方案12】:

                我喜欢Tatarize's answer,但我会提供另一种选择。这是一个微不足道的线性代数问题,我提出的方法适用于平移、缩放、倾斜等。也就是说,如果您的图像已经转换,它也很有效。

                当一个矩阵被缩放时,缩放在 (0, 0) 点。因此,如果您有一张图像并将其缩放 2 倍,则右下角的点将在 x 和 y 方向上加倍(使用 [0, 0] 是图像左上角的约定)。

                如果您想围绕中心缩放图像,则解决方案如下: (1) 平移图像,使其中心位于 (0, 0); (2) 通过 x 和 y 因子缩放图像; (3) 将图像翻译回来。即

                myMatrix
                  .translate(image.width / 2, image.height / 2)    // 3
                  .scale(xFactor, yFactor)                         // 2
                  .translate(-image.width / 2, -image.height / 2); // 1
                

                更抽象地说,相同的策略适用于任何点。例如,如果您想在 P 点缩放图像:

                myMatrix
                  .translate(P.x, P.y)
                  .scale(xFactor, yFactor)
                  .translate(-P.x, -P.y);
                

                最后,如果图像已经以某种方式进行了变换(例如,旋转、倾斜、平移或缩放),则需要保留当前变换。具体来说,上面定义的变换需要与当前变换进行后乘(或右乘)。

                myMatrix
                  .translate(P.x, P.y)
                  .scale(xFactor, yFactor)
                  .translate(-P.x, -P.y)
                  .multiply(myMatrix);
                

                你有它。 Here's a plunk 展示了这一点。在点上滚动鼠标滚轮,您会看到它们始终保持不变。 (仅在 Chrome 中测试。)http://plnkr.co/edit/3aqsWHPLlSXJ9JCcJzgH?p=preview

                【讨论】:

                • 我必须说,如果您有可用的仿射变换矩阵,请热情地使用它。很多变换矩阵甚至会有 zoom(sx,sy,x,y) 函数来做这件事。如果不给你一个使用,它几乎值得煮一个。
                • 事实上,我承认在我使用此解决方案的代码中,已将其替换为矩阵类。而且我已经多次做过这个确切的事情,并且已经制作了不少于两次的矩阵类。 (github.com/EmbroidePy/pyembroidery/blob/master/pyembroidery/…),(github.com/EmbroidePy/EmbroidePy/blob/master/embroidepy/…)。如果你想要比这些操作更复杂的东西,矩阵基本上是正确的答案,一旦你掌握了线性代数,你就会意识到这个答案实际上是最好的答案。
                • 我正在使用 Canvas API,但它没有直接的 multiply() API。相反,我一直在执行 resetTransform(),然后应用“缩放”平移、缩放、撤消缩放平移,然后应用实际所需的平移。这几乎可以工作,但有时会导致图像的原点移动。您能否提供一个示例,说明您如何使用 CanvasRenderingContext2D 对象执行上述操作?
                • @ScoPi 我写了以下文章,它提供了更多详细信息并有一个使用画布的示例:medium.com/@benjamin.botto/…
                【解决方案13】:

                一件重要的事情......如果你有类似的东西:

                body {
                  zoom: 0.9;
                }
                

                您需要在画布中制作等效的东西:

                canvas {
                  zoom: 1.1;
                }
                

                【讨论】:

                  【解决方案14】:

                  这是我用来更严格地控​​制绘制方式的一种方法

                  var canvas = document.getElementById("canvas");
                  var ctx = canvas.getContext("2d");
                  
                  var scale = 1;
                  var xO = 0;
                  var yO = 0;
                  
                  draw();
                  
                  function draw(){
                      // Clear screen
                      ctx.clearRect(0, 0, canvas.offsetWidth, canvas.offsetHeight);
                  
                      // Original coordinates
                      const xData = 50, yData = 50, wData = 100, hData = 100;
                      
                      // Transformed coordinates
                      const x = xData * scale + xO,
                       y = yData * scale + yO,
                       w = wData * scale,
                       h = hData * scale;
                  
                      // Draw transformed positions
                      ctx.fillStyle = "black";
                      ctx.fillRect(x,y,w,h);
                  }
                  
                  canvas.onwheel = function (e){
                      e.preventDefault();
                  
                      const r = canvas.getBoundingClientRect(),
                        xNode =  e.pageX - r.left,
                        yNode =  e.pageY - r.top;
                  
                      const newScale = scale * Math.exp(-Math.sign(e.deltaY) * 0.2),
                        scaleFactor = newScale/scale;
                  
                      xO = xNode - scaleFactor * (xNode - xO);
                      yO = yNode - scaleFactor * (yNode - yO);
                      scale = newScale;
                  
                      draw();
                  }
                  &lt;canvas id="canvas" width="600" height="200"&gt;&lt;/canvas&gt;

                  【讨论】:

                    【解决方案15】:

                    这是我的解决方案:

                    // helpers
                    const diffPoints = (p1, p2) => {
                        return {
                            x: p1.x - p2.x,
                            y: p1.y - p2.y,
                        };
                    };
                    
                    const addPoints = (p1, p2) => {
                        return {
                            x: p1.x + p2.x,
                            y: p1.y + p2.y,
                        };
                    };
                    
                    function scalePoint(p1, scale) {
                        return { x: p1.x / scale, y: p1.y / scale };
                    }
                    
                    // constants
                    const ORIGIN = Object.freeze({ x: 0, y: 0 });
                    const SQUARE_SIZE = 20;
                    const ZOOM_SENSITIVITY = 500; // bigger for lower zoom per scroll
                    const MAX_SCALE = 50;
                    const MIN_SCALE = 0.1;
                    
                    // dom
                    const canvas = document.getElementById("canvas");
                    const context = canvas.getContext("2d");
                    const debugDiv = document.getElementById("debug");
                    
                    // "props"
                    const initialScale = 0.75;
                    const initialOffset = { x: 10, y: 20 };
                    
                    // "state"
                    let mousePos = ORIGIN;
                    let lastMousePos = ORIGIN;
                    let offset = initialOffset;
                    let scale = initialScale;
                    
                    // when setting up canvas, set width/height to devicePixelRation times normal
                    const { devicePixelRatio = 1 } = window;
                    context.canvas.width = context.canvas.width * devicePixelRatio;
                    context.canvas.height = context.canvas.height * devicePixelRatio;
                    
                    function draw() {
                        window.requestAnimationFrame(draw);
                    
                        // clear canvas
                        context.canvas.width = context.canvas.width;
                    
                        // transform coordinates - scale multiplied by devicePixelRatio
                        context.scale(scale * devicePixelRatio, scale * devicePixelRatio);
                        context.translate(offset.x, offset.y);
                    
                        // draw
                        context.fillRect(200 + -SQUARE_SIZE / 2, 50 + -SQUARE_SIZE / 2, SQUARE_SIZE, SQUARE_SIZE);
                    
                        // debugging
                        context.beginPath();
                        context.moveTo(0, 0);
                        context.lineTo(0, 50);
                        context.moveTo(0, 0);
                        context.lineTo(50, 0);
                        context.stroke();
                        // debugDiv.innerText = `scale: ${scale}
                        // mouse: ${JSON.stringify(mousePos)}
                        // offset: ${JSON.stringify(offset)}
                        // `;
                    }
                    
                    // calculate mouse position on canvas relative to top left canvas point on page
                    function calculateMouse(event, canvas) {
                        const viewportMousePos = { x: event.pageX, y: event.pageY };
                        const boundingRect = canvas.getBoundingClientRect();
                        const topLeftCanvasPos = { x: boundingRect.left, y: boundingRect.top };
                        return diffPoints(viewportMousePos, topLeftCanvasPos);
                    }
                    
                    // zoom
                    function handleWheel(event) {
                        event.preventDefault();
                    
                        // update mouse position
                        const newMousePos = calculateMouse(event, canvas);
                        lastMousePos = mousePos;
                        mousePos = newMousePos;
                    
                        // calculate new scale/zoom
                        const zoom = 1 - event.deltaY / ZOOM_SENSITIVITY;
                        const newScale = scale * zoom;
                        if (MIN_SCALE > newScale || newScale > MAX_SCALE) {
                            return;
                        }
                    
                        // offset the canvas such that the point under the mouse doesn't move
                        const lastMouse = scalePoint(mousePos, scale);
                        const newMouse = scalePoint(mousePos, newScale);
                        const mouseOffset = diffPoints(lastMouse, newMouse);
                        offset = diffPoints(offset, mouseOffset);
                        scale = newScale;
                    }
                    canvas.addEventListener("wheel", handleWheel);
                    
                    // panning
                    const mouseMove = (event) => {
                        // update mouse position
                        const newMousePos = calculateMouse(event, canvas);
                        lastMousePos = mousePos;
                        mousePos = newMousePos;
                        const mouseDiff = scalePoint(diffPoints(mousePos, lastMousePos), scale);
                        offset = addPoints(offset, mouseDiff);
                    };
                    const mouseUp = () => {
                        document.removeEventListener("mousemove", mouseMove);
                        document.removeEventListener("mouseup", mouseUp);
                    };
                    const startPan = (event) => {
                        document.addEventListener("mousemove", mouseMove);
                        document.addEventListener("mouseup", mouseUp);
                        // set initial mouse position in case user hasn't moved mouse yet
                        mousePos = calculateMouse(event, canvas);
                    };
                    canvas.addEventListener("mousedown", startPan);
                    
                    // repeatedly redraw
                    window.requestAnimationFrame(draw);
                    #canvas {
                      /*set fixed width and height for what you actually want in css!*/
                      /*should be the same as what's passed into canvas element*/
                      width: 500px;
                      height: 150px;
                    
                      position: fixed;
                      border: 2px solid black;
                      top: 50%;
                      left: 50%;
                      transform: translate(-50%, -50%);
                    }
                    <!DOCTYPE html>
                    
                    <html lang="en">
                    <head>
                      <meta charset="utf-8" />
                      <meta name="viewport" content="width=device-width, initial-scale=1" />
                      <link rel="stylesheet" href="styles.css" />
                    </head>
                    
                    <body>
                    <!--still need width and height here, same as css-->
                    <canvas id="canvas" width="500" height="150"></canvas>
                    <div id="debug"></div>
                    <script type="module" src="pan_zoom.js"></script>
                    </body>
                    </html>

                    【讨论】:

                      【解决方案16】:

                      添加一个在 C# 和 WPF 中对我有用的答案:

                      double zoom = scroll > 0 ? 1.2 : (1/1.2);
                      
                      var CursorPosCanvas = e.GetPosition(Canvas);
                      pan.X += -(CursorPosCanvas.X - Canvas.RenderSize.Width / 2.0 - pan.X) * (zoom - 1.0);
                      pan.Y += -(CursorPosCanvas.Y - Canvas.RenderSize.Height / 2.0 - pan.Y) * (zoom - 1.0);
                      
                      transform.ScaleX *= zoom;
                      transform.ScaleY *= zoom;
                      

                      【讨论】:

                        猜你喜欢
                        • 2018-03-20
                        • 1970-01-01
                        • 1970-01-01
                        • 2022-09-29
                        • 2015-07-17
                        • 2014-11-07
                        • 1970-01-01
                        • 1970-01-01
                        • 2017-08-30
                        相关资源
                        最近更新 更多