将画布缩放至鼠标光标

91

我正在编写一个HTML5<canvas>的项目,它涉及使用滚轮放大和缩小图片。我希望像 Google 地图一样朝着光标缩放,但我完全不知道如何计算移动。

我拥有以下信息:图片的 x 和 y 坐标(左上角);图片的宽度和高度;相对于画布中心的光标的 x 和 y 坐标。


18
你应该接受这个答案或者修改你的问题。 - hayesgm
6个回答

277

简而言之,您想通过偏移量translate()画布上下文,scale()缩放以放大或缩小,然后通过鼠标偏移量的相反数translate()返回。请注意,您需要将光标位置从屏幕空间转换为转换后的画布上下文。

ctx.translate(pt.x,pt.y);
ctx.scale(factor,factor);
ctx.translate(-pt.x,-pt.y);

演示: http://phrogz.net/tmp/canvas_zoom_to_cursor.html

我在我的网站上放了一个完整的工作示例供您查看,支持拖动、点击缩放、按住Shift键点击缩小或使用滚轮向上/向下滚动。

唯一的(当前)问题是Safari相对于Chrome或Firefox缩放得太快了,详情请参见此处


4
例子非常好,做得很棒。谢谢! - Mikael Eliasson
6
哇,@phrogz,你超出了预期! - snapfractalpop
3
嘿@Phrogz,这太棒了!我只是想知道我们是否可以限制拖动,以便不能将图像拖出边界。如果没有更多的图像可以拖动,拖动就应该停在那里,而不是允许无限拖动。我试着去做但好像数学计算不对 :-( - Christoph
3
很简单,获取比例尺 - 您可以从中获取比例尺: var scale = ctx.getTransform().a; 然后获取图像的当前左上位置: var curX = ctx.getTransform().e; var curY = ctx.getTransform().f; 估算位置的变化: var deltaX = pt.x - dragStart.x; var deltaY = pt.y - dragStart.y; 接下来是原始图像大小,以宽度为例(当scale=1时):imageW 还有画布宽度:canvasW 然后条件应该是错误的,以允许拖动: curX + deltaX + imageW * scale < canvasW 还有一个 (curX + deltaX > 0 || curY + deltaY > 0) - maximus
2
这个在移动端使用手势实现起来难吗? 比如,允许用两个手指捏放来仅对图片进行缩放而不是整个网站? - Thomas Stock
显示剩余13条评论

15

13

最近我需要归档与Phrogz已经完成的相同结果,但是我没有使用context.scale(),而是根据比例计算每个对象的大小。

这是我想出来的方法。它背后的逻辑非常简单。在缩放之前,我计算距离边缘的点距离百分比,然后调整视口到正确的位置。

我花了很长时间才想出来这个方法,希望能节省一些人的时间。

$(function () {
  var canvas = $('canvas.main').get(0)
  var canvasContext = canvas.getContext('2d')

  var ratio = 1
  var vpx = 0
  var vpy = 0
  var vpw = window.innerWidth
  var vph = window.innerHeight

  var orig_width = 4000
  var orig_height = 4000

  var width = 4000
  var height = 4000

  $(window).on('resize', function () {
    $(canvas).prop({
      width: window.innerWidth,
      height: window.innerHeight,
    })
  }).trigger('resize')

  $(canvas).on('wheel', function (ev) {
    ev.preventDefault() // for stackoverflow

    var step

    if (ev.originalEvent.wheelDelta) {
      step = (ev.originalEvent.wheelDelta > 0) ? 0.05 : -0.05
    }

    if (ev.originalEvent.deltaY) {
      step = (ev.originalEvent.deltaY > 0) ? 0.05 : -0.05
    }

    if (!step) return false // yea..

    var new_ratio = ratio + step
    var min_ratio = Math.max(vpw / orig_width, vph / orig_height)
    var max_ratio = 3.0

    if (new_ratio < min_ratio) {
      new_ratio = min_ratio
    }

    if (new_ratio > max_ratio) {
      new_ratio = max_ratio
    }

    // zoom center point
    var targetX = ev.originalEvent.clientX || (vpw / 2)
    var targetY = ev.originalEvent.clientY || (vph / 2)

    // percentages from side
    var pX = ((vpx * -1) + targetX) * 100 / width
    var pY = ((vpy * -1) + targetY) * 100 / height

    // update ratio and dimentsions
    ratio = new_ratio
    width = orig_width * new_ratio
    height = orig_height * new_ratio

    // translate view back to center point
    var x = ((width * pX / 100) - targetX)
    var y = ((height * pY / 100) - targetY)

    // don't let viewport go over edges
    if (x < 0) {
      x = 0
    }

    if (x + vpw > width) {
      x = width - vpw
    }

    if (y < 0) {
      y = 0
    }

    if (y + vph > height) {
      y = height - vph
    }

    vpx = x * -1
    vpy = y * -1
  })

  var is_down, is_drag, last_drag

  $(canvas).on('mousedown', function (ev) {
    is_down = true
    is_drag = false
    last_drag = { x: ev.clientX, y: ev.clientY }
  })

  $(canvas).on('mousemove', function (ev) {
    is_drag = true

    if (is_down) {
      var x = vpx - (last_drag.x - ev.clientX)
      var y = vpy - (last_drag.y - ev.clientY)

      if (x <= 0 && vpw < x + width) {
        vpx = x
      }

      if (y <= 0 && vph < y + height) {
        vpy = y
      }

      last_drag = { x: ev.clientX, y: ev.clientY }
    }
  })

  $(canvas).on('mouseup', function (ev) {
    is_down = false
    last_drag = null

    var was_click = !is_drag
    is_drag = false

    if (was_click) {

    }
  })

  $(canvas).css({ position: 'absolute', top: 0, left: 0 }).appendTo(document.body)

  function animate () {
    window.requestAnimationFrame(animate)

    canvasContext.clearRect(0, 0, canvas.width, canvas.height)

    canvasContext.lineWidth = 1
    canvasContext.strokeStyle = '#ccc'

    var step = 100 * ratio

    for (var x = vpx; x < width + vpx; x += step) {
      canvasContext.beginPath()
      canvasContext.moveTo(x, vpy)
      canvasContext.lineTo(x, vpy + height)
      canvasContext.stroke()
    }
    for (var y = vpy; y < height + vpy; y += step) {
      canvasContext.beginPath()
      canvasContext.moveTo(vpx, y)
      canvasContext.lineTo(vpx + width, y)
      canvasContext.stroke()
    }

    canvasContext.strokeRect(vpx, vpy, width, height)

    canvasContext.beginPath()
    canvasContext.moveTo(vpx, vpy)
    canvasContext.lineTo(vpx + width, vpy + height)
    canvasContext.stroke()

    canvasContext.beginPath()
    canvasContext.moveTo(vpx + width, vpy)
    canvasContext.lineTo(vpx, vpy + height)
    canvasContext.stroke()

    canvasContext.restore()
  }

  animate()
})
<!DOCTYPE html>
<html>
<head>
 <title></title>
 <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
</head>
<body>
 <canvas class="main"></canvas>
</body>
</html>


7

我以@Phrogz的答案为基础,制作了一个小型库,使画布具有拖动、缩放和旋转功能。这是一个示例。

var canvas = document.getElementById('canvas')
//assuming that @param draw is a function where you do your main drawing.
var control = new CanvasManipulation(canvas, draw)
control.init()
control.layout()
//now you can drag, zoom and rotate in canvas

您可以在该项目的页面上找到更详细的示例和文档。


5

更快

使用 ctx.setTransform 比多次调用矩阵方法 ctx.translate, ctx.scale, ctx.translate 更加高效。

不需要进行复杂的转换反演或者昂贵的DOM矩阵调用来在缩放和屏幕坐标系之间转换点。

更灵活

更灵活,如果您正在使用不同的变换渲染内容,则无需使用 ctx.savectx.restore。只需使用 ctx.setTransform 回到变换即可,而不是可能会降低帧率的 ctx.restore 调用。

轻松地反转变换并获取一个(屏幕)像素位置的世界坐标以及反之亦然。

示例

使用鼠标和鼠标滚轮在鼠标位置缩放页面内容

使用此方法的示例,通过 CSS 变换在一个点(鼠标)缩放页面内容 CSS演示在答案底部也有下一个示例的演示副本。

以及使用此方法对使用 setTransform 缩放画布内容的示例

如何

给定一个缩放比例和像素位置,您可以按如下方式获取新的比例...

const origin = {x:0, y:0};         // canvas origin
var scale = 1;                     // current scale
function scaleAt(x, y, scaleBy) {  // at pixel coords x, y scale by scaleBy
    scale *= scaleBy;
    origin.x = x - (x - origin.x) * scaleBy;
    origin.y = y - (y - origin.y) * scaleBy;
}

定位画布并绘制内容。
ctx.setTransform(scale, 0, 0, scale, origin.x, origin.y);
ctx.drawImage(img, 0, 0);

如果你有鼠标坐标,可以这样使用

const zoomBy = 1.1;                    // zoom in amount
scaleAt(mouse.x, mouse.y, zoomBy);     // will zoom in at mouse x, y
scaleAt(mouse.x, mouse.y, 1 / zoomBy); // will zoom out by same amount at mouse x,y

恢复默认的变换
ctx.setTransform(1,0,0,1,0,0);

反演

获取缩放坐标系中一个点的坐标和屏幕上该点的位置。

屏幕到世界

function toWorld(x, y) {  // convert to world coordinates
    x = (x - origin.x) / scale;
    y = (y - origin.y) / scale;
    return {x, y};
}

从世界到屏幕

function toScreen(x, y) {
    x = x * scale + origin.x;
    y = y * scale + origin.y;
    return {x, y};
}

0

我已经修改了@Phrogz的解决方案(本身就很棒),并增加了以下功能:

  1. 允许一定范围内的缩放级别(不是无限制)
  2. 默认或从最大缩小级别开始
  3. 允许多层画布对象(同时缩放数组中的所有画布对象)
  4. 缩小不会显示原始视图之外的区域。

希望这能帮助到某些人。

    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 = 20;

    let scaleFactor = 1.1;
    let zoomLevel = 0;

    let zoom = function(clicks) {
        if( zoomLevel + clicks < MIN_ZOOM_LEVEL ||
            zoomLevel + clicks > MAX_ZOOM_LEVEL ) { return; }

        let factor = Math.pow(scaleFactor,clicks);

        for( let i = 0 ; i < context.length ; ++i ) {
            let pt = context[i].transformedPoint(lastX,lastY);

            context[i].translate( pt.x, pt.y );
            context[i].scale( factor, factor );
            context[i].translate( -pt.x, -pt.y );

            let adjust = false;
            pt = context[i].transformedPoint( 0, 0 );
            if( pt.x < 0 ) { adjust = true; }
            if( pt.y < 0 ) { adjust = true; }

            if( adjust ) {
                context[i].translate( pt.x, pt.y );
                adjust = false;
            }

            pt = context[i].transformedPoint( layer[i].width, layer[i].height );
            if( pt.x > layer[i].width ) { adjust = true; }
            if( pt.y > layer[i].height ) { adjust = true; }

            if( adjust ) {
                context[i].translate( pt.x - layer[i].width, pt.y - layer[i].height );
                adjust = false;
            }
        }

        zoomLevel += clicks;
        draw();
    }

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