缩放光标 - 语言不可知

Zooming on a cursor - language agnostic

我在绘图应用程序中实现了缩放功能,但它总是从左上角开始 zooms/unzooms。图片会比我解释得更好...
绘图区域是无限网格。绿色区域是当前显示在屏幕上的网格部分。当用户平移相机时,scrollPosition 的坐标会发生变化,从而移动查看区域。当用户缩放时,它会更改 pixelsPerInch 变量。我怎样才能使相机在缩放时真正以光标为中心?我也希望能够取消缩放,如果我只是不停地缩放和取消缩放而不移动光标,我应该总是在同一个地方结束。

编辑:我拥有的变量是 scrollPosition(带有 x 和 y 的对象)、pixelsPerInch、屏幕上实际 window 的宽度和高度、光标(x 和 y)。我正在寻找一种方法来在更改 pixelsPerInch 时设置 scrollPosition 的新值,以便光标成为新放大区域的中心。


我是怎么解决的:

我从头开始重建了一个可拖动和可缩放的 canvas 系统,以确保隐藏在原始代码中的错误不是导致我出现问题的原因。然后,我花了更多时间来写下我想做的事情。这样应该就清楚多了。

我做了一个函数,将鼠标的坐标转换为底层无限表面上的坐标。它基本上采用 window 上的光标位置,将其乘以当前比例因子,然后减去原点偏移量(我在之前的绘图中称为 scrollPosition)。

function relative(canvasCoord) {
    return {x: canvasCoord.x/ppi - offset.x, y: canvasCoord.y/ppi - offset.y };
}

这让我可以在应用缩放前后检查光标位置。然后我可以计算两个坐标之间的差异并将该差异应用于偏移量。 Javascript代码:

context.canvas.addEventListener("wheel", function(e) {
    var canvasPoint = getCanvasCoordinates(e.pageX, e.pageY);
    var oldPoint = relative(canvasPoint);

    var scrollDirection = -Math.min(1, Math.max(-1, e.deltaY));
    ppi += scrollDirection / 10;

    var newPoint = relative(canvasPoint);
    offset.x += newPoint.x - oldPoint.x;
    offset.y += newPoint.y - oldPoint.y;
    blit();
});

这就是让我意识到我实际上是以基于视图的像素而不是原始代码中的英寸为单位存储偏移量的原因,这在我实现缩放之前没有造成任何问题。愚蠢的错误。但从头开始确实帮助我理解了视口。这是一个仅适用于 HTML5 浏览器的快速演示(可能无法在 chrome 之外工作,我的本地版本中有用于该浏览器的 polyfill 脚本)。

JS fiddle 如果您发现它比 SO 片段更人性化:http://jsfiddle.net/buu7h0be/5/

context = document.getElementById("myCanvas").getContext("2d");
context.fillStyle = "#5555FF";
context.imageSmoothingEnabled = context.webkitImageSmoothingEnabled = context.mozImageSmoothingEnabled;
ppi = 1; // pixels per inch
shapes = [];
offset = {x:0, y:0};
cursor = {x:0, y:0};
isDragging = false;

function Rectangle(x, y, width, height) {
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
}

Rectangle.prototype.getLeft = function() { return this.x - width/2; };
Rectangle.prototype.getRight = function() { return this.x + width/2; };
Rectangle.prototype.getTop = function() { return this.y - height/2; };
Rectangle.prototype.getBottom = function() { return this.y + height/2; };

Rectangle.prototype.draw = function() {
    context.save();
    context.translate(this.x * ppi, this.y * ppi);
    var w = this.width * ppi,
        h = this.height * ppi;
    context.fillRect(-w/2, -h/2, w, h);
    context.restore();
}

function clear() {
    context.save();
    context.setTransform(1, 0, 0, 1, 0, 0);
    context.clearRect(0, 0, context.canvas.width, context.canvas.height);
    context.restore();
}

function blit() {
    context.save();
    clear();
    context.translate(offset.x * ppi, offset.y * ppi);
    for(var i=0; i<shapes.length; ++i) {
        shapes[i].draw();
    }
    context.restore();
}

// relative coordinates : in inches and relative to 0,0 on the imaginary infinite surface
// canvas coordinates : in pixels and relative to the top left corner of the canvas element

function getCanvasCoordinates(pageX, pageY) {
  var rect = context.canvas.getBoundingClientRect();
  return {x: pageX - rect.left - document.body.scrollLeft, y: pageY - rect.top - document.body.scrollTop};
}

function relative(canvasCoord) {
    return {x: canvasCoord.x/ppi - offset.x, y: canvasCoord.y/ppi - offset.y };
}


context.canvas.addEventListener("mousedown", function(e) {
    isDragging = true;
    cursor = getCanvasCoordinates(e.pageX, e.pageY);
});

context.canvas.addEventListener("mousemove", function(e) {
    var newcursor = getCanvasCoordinates(e.pageX, e.pageY);
    if(isDragging) {
        offset.x += (newcursor.x - cursor.x) / ppi;
        offset.y += (newcursor.y - cursor.y) / ppi;
    }
    cursor = newcursor;
    blit();
});

context.canvas.addEventListener("mouseup", function(e) {
    isDragging = false;
});


context.canvas.addEventListener("wheel", function(e) {
    var canvasPoint = getCanvasCoordinates(e.pageX, e.pageY);
    var oldPoint = relative(canvasPoint);
    
    var scrollDirection = -Math.min(1, Math.max(-1, e.deltaY));
    ppi += scrollDirection / 10;
    
    var newPoint = relative(canvasPoint);
    offset.x += newPoint.x - oldPoint.x;
    offset.y += newPoint.y - oldPoint.y;
    blit();
});

context.canvas.addEventListener("keydown", function(e) {
    switch(e.keyCode) {
        case 107: //add (numpad)
            ppi += 0.05
            break;
        case 109: //subtract (numpad)
            ppi -= 0.05;
            break;
        case 37: //left
            offset.x -= 10;
            break;
        case 39: //right
            offset.x += 10;
            break;
        case 38: //up
            offset.y -= 10;
            break;
        case 40: //down
            offset.y += 10;
            break;
    }
    blit();
});


shapes.push(new Rectangle(100, 100, 100, 100));
shapes.push(new Rectangle(400, 200, 75, 150));
shapes.push(new Rectangle(200, 400, 175, 95));
blit();
canvas {
    border: 0px solid transparent;
    outline: 1px solid silver;
    cursor: move;
}
<canvas id="myCanvas" width="500" height="500" tabindex="0">
    Your browser is not compatible with the HTML5 canvas.
</canvas>

好的,以一种与语言无关的方式......向后解决问题。

您需要计算的是绘制场景的左上角位置(原点),以便场景上的选定点(光标)始终位于设备显示屏上的同一视觉点。

例如假设你的场景:

  • 它的左上原点在 [0,0],
  • 按 100% 大小缩放,
  • 所选(光标)点为[100,50]。

向后计算,您的原点必须位于相对于所选点的 [-100,-50] 处。换句话说,左上角原点或您的场景相对于光标点的 X 偏移为 -100,Y 偏移为 -50。

如果将场景缩放 200%,则双倍大小的场景必须以原始 [-100,-50] 偏移量的 2 倍绘制:

// the new originX & originY is [-200,-100]
originX = -100 * 2.00 
originY =  -50 * 2.00

因此,如果您在 [-200,-100] 处绘制双倍大小的场景,那么即使您的场景是原来的两倍大,您的光标点也会位于设备显示屏上的相同位置。

因此,对于任何大小的场景,都可以缩放:

// calculate the required top-left of your scene for any given scaleFactor
originX = -100 * scaleFactor/100
originY =  -50 * scaleFactor/100