Javascript 放大一个点(使用缩放和平移)
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/2916081/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Zoom in on a point (using scale and translate)
提问by csiz
I want to be able to zoom in on the point under the mouse in an HTML 5 canvas, like zooming on Google Maps. How can I achieve that?
我希望能够放大 HTML 5 画布中鼠标下方的点,例如放大Google Maps。我怎样才能做到这一点?
采纳答案by csiz
Finally solved it:
终于解决了:
var zoomIntensity = 0.2;
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
var width = 600;
var height = 200;
var scale = 1;
var originx = 0;
var originy = 0;
var visibleWidth = width;
var visibleHeight = height;
function draw(){
// Clear screen to white.
context.fillStyle = "white";
context.fillRect(originx,originy,800/scale,600/scale);
// Draw the black square.
context.fillStyle = "black";
context.fillRect(50,50,100,100);
}
// Draw loop at 60FPS.
setInterval(draw, 1000/60);
canvas.onwheel = function (event){
event.preventDefault();
// Get mouse offset.
var mousex = event.clientX - canvas.offsetLeft;
var mousey = event.clientY - canvas.offsetTop;
// Normalize wheel to +1 or -1.
var wheel = event.deltaY < 0 ? 1 : -1;
// Compute zoom factor.
var 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.
正如@Tatarize 指出的那样,关键是计算轴位置,以便缩放点(鼠标指针)在缩放后保持在同一位置。
Originally the mouse is at a distance mouse/scalefrom the corner, we want the point under the mouse to remain in the same place after the zoom, but this is at mouse/new_scaleaway from the corner. Therefore we need to shift the origin(coordinates of the corner) to account for this.
本来鼠标是离mouse/scale拐角有一定距离的,我们希望鼠标下的点在缩放后保持在原来的位置,但是这个点是mouse/new_scale离拐角远的。因此,我们需要移动origin(角的坐标)来解决这个问题。
originx -= mousex/(scale*zoom) - mousex/scale;
originy -= mousey/(scale*zoom) - mousey/scale;
scale *= zoom
The remaining code then needs to apply the scaling and translate to the draw context so it's origin coincides with the canvas corner.
然后剩下的代码需要应用缩放并转换到绘制上下文,使其原点与画布角重合。
回答by Tatarize
The better solution is to simply move the position of the viewport based on the change in the zoom. The zoom point is simply the point in the old zoom and the new zoom that you want to remain the same. Which is to say the viewport pre-zoomed and the viewport post-zoomed have the same zoompoint relative to the viewport. Given that we're scaling relative to the origin. You can adjust the viewport position accordingly:
更好的解决方案是根据缩放的变化简单地移动视口的位置。缩放点只是您希望保持不变的旧缩放和新缩放中的点。也就是说,预先缩放的视口和缩放后的视口相对于视口具有相同的缩放点。鉴于我们相对于原点进行缩放。您可以相应地调整视口位置:
scalechange = newscale - oldscale;
offsetX = -(zoomPointX * scalechange);
offsetY = -(zoomPointY * scalechange);
So really you can just pan over down and to the right when you zoom in, by a factor of how much you zoomed in, relative to the point you zoomed at.
因此,实际上您可以在放大时向下和向右平移,根据放大的程度,相对于放大的点。
回答by Sunday Ironfoot
This is actually a very difficult problem (mathematically), and I'm working on the same thing almost. I asked a similar question on Stackoverflow but got no response, but posted in DocType (StackOverflow for HTML/CSS) and got a response. Check it out http://doctype.com/javascript-image-zoom-css3-transforms-calculate-origin-example
这实际上是一个非常困难的问题(数学上),我几乎在做同样的事情。我在 Stackoverflow 上问了一个类似的问题,但没有得到回应,但在 DocType (StackOverflow for HTML/CSS) 中发布并得到了回应。查看http://doctype.com/javascript-image-zoom-css3-transforms-calculate-origin-example
I'm in the middle of building a jQuery plugin that does this (Google Maps style zoom using CSS3 Transforms). I've got the zoom to mouse cursor bit working fine, still trying to figure out how to allow the user to drag the canvas around like you can do in Google Maps. When I get it working I'll post code here, but check out above link for the mouse-zoom-to-point part.
我正在构建一个执行此操作的 jQuery 插件(使用 CSS3 转换的 Google 地图样式缩放)。我已经将鼠标光标缩放到位工作正常,仍在尝试弄清楚如何允许用户像在 Google 地图中那样拖动画布。当我让它工作时,我会在这里发布代码,但请查看上面的鼠标缩放到点部分的链接。
I didn't realise there was scale and translate methods on Canvas context, you can achieve the same thing using CSS3 eg. using jQuery:
我没有意识到 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)');
Make sure you set the CSS3 transform-origin to 0, 0 (-moz-transform-origin: 0 0). Using CSS3 transform allows you to zoom in on anything, just make sure the container DIV is set to overflow: hidden to stop the zoomed edges spilling out of the sides.
确保将 CSS3 transform-origin 设置为 0, 0 (-moz-transform-origin: 0 0)。使用 CSS3 变换可以放大任何内容,只需确保容器 DIV 设置为 overflow: hidden 以阻止缩放的边缘从侧面溢出。
Whether you use CSS3 transforms, or canvas' own scale and translate methods is upto you, but check the above link for the calculations.
无论您使用 CSS3 变换还是画布自己的缩放和翻译方法都由您决定,但请查看上面的链接以进行计算。
Update:Meh! I'll just post the code here rather than get you to follow a link:
更新:嗯!我只会在此处发布代码,而不是让您点击链接:
$(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;
});
});
You will of course need to adapt it to use the canvas scale and translate methods.
您当然需要调整它以使用画布比例和翻译方法。
Update 2:Just noticed I'm using transform-origin together with translate. I've managed to implement a version that just uses scale and translate on their own, check it out here http://www.dominicpettifer.co.uk/Files/Mosaic/MosaicTest.htmlWait for the images to download then use your mouse wheel to zoom, also supports panning by dragging the image around. It's using CSS3 Transforms but you should be able to use the same calculations for your Canvas.
更新 2:刚刚注意到我正在使用 transform-origin 和 translate。我已经设法实现了一个仅使用比例尺并自行翻译的版本,请在此处查看http://www.dominicpettifer.co.uk/Files/Mosaic/MosaicTest.html等待图像下载然后使用您的鼠标滚轮缩放,还支持通过拖动图像来平移。它使用 CSS3 Transforms,但您应该能够对 Canvas 使用相同的计算。
回答by Aleksandr Albert
I ran into this problem using c++, which I probably shouldn't have had i just used OpenGL matrices to begin with...anyways, if you're using a control whose origin is the top left corner, and you want pan/zoom like google maps, here's the layout (using allegro as my event handler):
我使用 c++ 遇到了这个问题,我可能不应该让我刚开始使用 OpenGL 矩阵...无论如何,如果您使用的是原点位于左上角的控件,并且您想要平移/缩放像谷歌地图一样,这是布局(使用快板作为我的事件处理程序):
// 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;
}
回答by avejidah
I like Tatarize's answer, but I'll provide an alternative. This is a trivial linear algebra problem, and the method I present works well with pan, zoom, skew, etc. That is, it works well if your image is already transformed.
我喜欢Tatarize 的回答,但我会提供一个替代方案。这是一个微不足道的线性代数问题,我提出的方法适用于平移、缩放、倾斜等。也就是说,如果您的图像已经被转换,它就可以很好地工作。
When a matrix is scaled, the scale is at point (0, 0). So, if you have an image and scale it by a factor of 2, the bottom-right point will double in both the x and y directions (using the convention that [0, 0] is the top-left of the image).
缩放矩阵时,缩放点位于 (0, 0) 点。因此,如果您有一个图像并将其缩放 2 倍,则右下角的点将在 x 和 y 方向上加倍(使用 [0, 0] 是图像左上角的约定)。
If instead you would like to zoom the image about the center, then a solution is as follows: (1) translate the image such that its center is at (0, 0); (2) scale the image by x and y factors; (3) translate the image back. i.e.
相反,如果您想围绕中心缩放图像,那么解决方案如下: (1) 平移图像,使其中心位于 (0, 0);(2) 按 x 和 y 因子缩放图像;(3) 将图像翻译回来。IE
myMatrix
.translate(image.width / 2, image.height / 2) // 3
.scale(xFactor, yFactor) // 2
.translate(-image.width / 2, -image.height / 2); // 1
More abstractly, the same strategy works for any point. If, for example, you want to scale the image at a point P:
更抽象地说,相同的策略适用于任何一点。例如,如果您想在 P 点缩放图像:
myMatrix
.translate(P.x, P.y)
.scale(xFactor, yFactor)
.translate(-P.x, -P.y);
And lastly, if the image is already transformed in some manner (for example, if it's rotated, skewed, translated, or scaled), then the current transformation needs to be preserved. Specifically, the transform defined above needs to be post-multiplied (or right-multiplied) by the current transform.
最后,如果图像已经以某种方式进行了变换(例如,如果它被旋转、倾斜、平移或缩放),则需要保留当前的变换。具体来说,上面定义的变换需要与当前变换进行后乘(或右乘)。
myMatrix
.translate(P.x, P.y)
.scale(xFactor, yFactor)
.translate(-P.x, -P.y)
.multiply(myMatrix);
There you have it. Here's a plunk that shows this in action. Scroll with the mousewheel on the dots and you'll see that they consistently stay put. (Tested in Chrome only.) http://plnkr.co/edit/3aqsWHPLlSXJ9JCcJzgH?p=preview
你有它。这是一个 plunk,显示了这一点。用鼠标滚轮在点上滚动,您会看到它们始终保持原状。(仅在 Chrome 中测试。)http://plnkr.co/edit/3aqsWHPLlSXJ9JCcJzgH?p=preview
回答by Chris
Here's my solution for a center-oriented image:
这是我的面向中心图像的解决方案:
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="http://s18.postimg.org/eplac6dbd/mountain.jpg">
</div>
回答by chad
Here's an alternate way to do it that uses setTransform() instead of scale() and translate(). Everything is stored in the same object. The canvas is assumed to be at 0,0 on the page, otherwise you'll need to subtract its position from the page coords.
这是一种替代方法,它使用 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);
};
Accompanying code to handle panning:
处理平移的附带代码:
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
};
};
To derive the answer yourself, consider that the same page coordinates need to match the same canvas coordinates before and after the zoom. Then you can do some algebra starting from this equation:
要自己得出答案,请考虑相同的页面坐标需要在缩放前后匹配相同的画布坐标。然后你可以从这个方程开始做一些代数:
(pageCoords - translation) / scale = canvasCoords
(pageCoords - 翻译) / 比例 = canvasCoords
回答by Vasiliy Stavenko
I want to put here some information for those, who do separately drawing of picture and moving -zooming it.
我想在这里为那些单独绘制图片和移动缩放它的人提供一些信息。
This may be useful when you want to store zooms and position of viewport.
当您想要存储视口的缩放和位置时,这可能很有用。
Here is drawer:
这是抽屉:
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!!!
}
Notice scale MUST be first.
注意scale 必须是 first。
And here is 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
}
and, of course, we would need a dragger:
当然,我们还需要一个拖动器:
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();
}
})
回答by Daniel
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);
}
回答by Mirror318
Here's a code implementation of @tatarize's answer, using PIXI.js. I have a viewport looking at part of a very big image (e.g. google maps style).
这是@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);
});
$canvasContaineris my html container.imageContaineris my PIXI container that has the image in it.mousePosOnImageis the mouse position relative to the entire image (not just the view port).
$canvasContainer是我的 html 容器。imageContainer是我的 PIXI 容器,里面有图像。mousePosOnImage是相对于整个图像(不仅仅是视口)的鼠标位置。
Here's how I got the mouse position:
这是我获得鼠标位置的方法:
imageContainer.on('mousemove', _.bind(function(ev) {
mousePosOnImage = ev.data.getLocalPosition(imageContainer);
mousePosOnViewport.x = ev.data.originalEvent.offsetX;
mousePosOnViewport.y = ev.data.originalEvent.offsetY;
},self));


