放大一个点(使用缩放和平移)

我希望能够放大在一个 HTML 5画布鼠标下的点,就像在 谷歌地图上放大。我怎么才能做到呢?

166582 次浏览

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

我正在构建一个 jQuery 插件来实现这个功能(使用 CSS3 TransformsGoogle 地图样式缩放)。我已经把鼠标放大到光标位工作得很好,仍然试图弄清楚如何让用户拖动画布,就像你可以在谷歌地图。当我得到它的工作,我将在这里发布代码,但检查上面的链接鼠标放大到点部分。

我没有意识到在 Canvas 上下文中有伸缩和翻译方法,你可以使用 CSS3实现同样的功能,比如使用 jQuery:

$('div.canvasContainer > canvas')
.css('transform', 'scale(1) translate(0px, 0px)');

确保将 CSS3转换原点设置为0,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('transform', 'scale(' + scale + ')' + 'translate(' + xNew + 'px, ' + yNew + 'px' + ')')
.css('transform-origin', xImage + 'px ' + yImage + 'px')
return false;
});
});

当然,您需要对其进行调整,以使用画布缩放和转换方法。

终于解决了:

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

The key, as @Tatarize pointed out, is to compute the axis position such that the zoom point (mouse pointer) remains in the same place after the zoom.

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 *= zoom

剩下的代码需要应用缩放并转换到绘制上下文,这样它的原点就会与画布角重合。

我想在这里提供一些信息给那些分别画图和移动缩放的人。

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

这是抽屉:

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!!!
}

通知 规模必须是第一

这是 Zoomer:

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


}




})

我在使用 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;
}

这里有一种替代方法,它使用 setTransform ()而不是 scale ()和 trans ()。所有东西都存储在同一个对象中。假设画布在页面上的位置为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 = 画布

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

更好的解决方案是根据缩放的变化简单地移动视口的位置。缩放点仅仅是您希望保持不变的旧缩放和新缩放中的点。也就是说,视口预缩放和视口后缩放相对于视口具有相同的缩放点。考虑到我们相对于原点的比例。您可以相应地调整视图位置:

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

所以当你放大的时候,你可以向下和向右移动,相对于你放大的点,你放大了多少。

enter image description here

您需要在缩放之前和之后获得世界空间中的点(与屏幕空间相对) ,然后通过增量进行转换。

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

以下是我对以中心为导向的图像的解决方案:

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>

您可以使用 scrollto (x,y)函数来处理滚动条的位置,直到在 zooming.之后需要显示的位置,以便找到鼠标的位置,使用 event.clientX 和 event.clientY。 这个能帮到你

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

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

我喜欢 鞑靼人的回答,但我会提供另一种选择。这是一个简单的线性代数问题,我提出的方法可以很好地处理平移、缩放、倾斜等问题。也就是说,如果您的图像已经被转换,那么它将工作得很好。

当矩阵缩放时,缩放点为(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);

就是这样。这里有一个可以展示这种行为的砰砰声。用鼠标滚轮在圆点上滚动,你会看到它们始终保持不动。(只在 Chrome 中测试)http://plnkr.co/edit/3aqsWHPLlSXJ9JCcJzgH?p=preview

一件重要的事... 如果你有这样的东西:

body {
zoom: 0.9;
}

你需要在画布上做出等价的东西:

canvas {
zoom: 1.1;
}

这里有一个方法,我用来更严格地控制事物是如何绘制的

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();
}
<canvas id="canvas" width="600" height="200"></canvas>

我的解决办法是:

// 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>

在 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;