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


156

我希望能够在HTML 5画布中的鼠标下方放大该点,就像在Google Maps上放大一样。我该如何实现?


2
我用它来放大画布,效果很好!我唯一要添加的是,缩放量的计算与您期望的不同。“ var zoom = 1 + wheel / 2;” 即,这将导致放大1.5和缩小0.5。我在我的版本中对此进行了编辑,以使我有1.5的放大率和1 / 1.5的缩小率,这使放大和缩小的量相等。因此,如果放大一次并向后缩放,您将获得与缩放之前相同的图片。
克里斯,

2
请注意,这在Firefox上不起作用,但是该方法可以轻松地应用于jQuery mousewheel plugin。感谢分享!
johndodo

2
var zoom = Math.pow(1.5f,wheel); //使用此来计算缩放。这样做的好处是,用wheel = 2进行缩放与通过wheel = 1进行两次缩放相同。此外,按+2放大和按+2缩小可恢复原始比例。
马特

Answers:


126

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

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

因此,实际上,放大时只是相对于放大的点,就可以上下左右平移。

在此处输入图片说明


2
比剪切和粘贴代码更有价值的是,解释最佳解决方案是什么,以及为什么不用行李也能工作,尤其是三行长的情况下。
2015年

2
scalechange = newscale / oldscale?
2015年

4
另外,我想补充一些寻求实现像pan-zoom组件这样的地图的功能,鼠标X,Y应该是(mousePosRelativeToContainer-currentTransform)/ currentScale,否则它将相对于容器对待当前鼠标位置。
吉拉德(Gilad)

1
是的,此数学假设缩放和平移在与原点相关的坐标中。如果它们相对于视口,则必须适当调整它们。尽管我认为正确的数学运算是zoomPoint =(mousePosRelativeToContainer + currentTranslation)。此数学还假设起点通常位于字段的左上方。但是,鉴于简单性,针对非典型情况进行调整要容易得多。
Ta

1
@ C.Finke第二种方法是使用ctx中的转换。您以相同的尺寸和相同的位置绘制所有内容。但是,您只需在javascript画布内使用矩阵乘法来设置上下文的平移和缩放(缩放)即可。因此,与其在所有位置重绘所有形状,不如将它们重绘。您将它们绘制在同一位置,然后在javascript中移动视口。此方法还需要您获取鼠标事件并向后转换它们。因此,您需要减去平移,然后通过缩放反转因子。
Tatarize

67

终于解决了:

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>

@Tata​​rize指出,关键是计算轴位置,以便缩放点(鼠标指针)在缩放后保持在同一位置。

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

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

然后,其余代码需要应用缩放并转换到绘制上下文,以便其原点与画布角重合。


谢谢您,伙计,几乎失去了2天,然后才找到您的代码
Velaro

嘿,我只是在寻找这样的东西,只是想说一声让您破解了!
chrisallick

26

(从数学上来说)这实际上是一个非常困难的问题,我几乎正在研究同一件事。我在Stackoverflow上问了类似的问题,但没有任何响应,但是在DocType中发布(对于HTML / CSS为StackOverflow)并得到了响应。检查一下http://doctype.com/javascript-image-zoom-css3-transforms-calculate-origin-example

我正在构建执行此操作的jQuery插件(使用CSS3 Transforms进行Google Maps样式缩放)。我已经将鼠标光标缩放到可以正常工作的位置,但仍在尝试找出如何允许用户像在Google Maps中一样拖动画布。当它正常工作时,我将在此处发布代码,但请查看上面的链接,了解鼠标缩放到点的部分。

我没有意识到在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转换源设置为0、0(-moz-transform-origin:0 0)。使用CSS3变换,您可以放大所有内容,只需确保将容器DIV设置为溢出:隐藏即可阻止缩放的边缘溢出到侧面。

是否使用CSS3转换,还是canvas自己的缩放和转换方法由您自己决定,但是请查看上面的链接进行计算。


更新:嗯!我将代码发布在这里,而不是让您跟随链接:

$(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:刚注意到我正在使用转换原点和翻译。我已经设法实现了一个仅使用比例尺和自行翻译的版本,请在此处查看它http://www.dominicpettifer.co.uk/Files/Mosaic/MosaicTest.html等待图像下载然后使用您的鼠标滚轮进行缩放,还支持通过拖动图像来进行平移。它使用CSS3转换,但您应该能够对Canvas使用相同的计算。


我终于解决了,经过大约2周的时间,我现在花了3分钟
csiz 2010年

@Synday Ironfoot的更新链接无效。这个链接:dominicpettifer.co.uk/Files/Mosaic/MosaicTest.html 我想要这种实现。您可以在此处发布代码吗?谢谢
Bogz 2014年

2
截至今天(2014年9月),指向MosaicTest.html的链接已失效。
克里斯

马赛克演示不见了。我通常使用香草js,而不是jQuery。$(this)指的是什么?document.body.offsetTop?我真的很想看到我的foreverscape.com项目的镶嵌演示可以真正从中受益。
FlavorScape

2
:马赛克演示页面保存在archive.org web.archive.org/web/20130126152008/http://...
克里斯

9

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

9

我喜欢Tatarize的答案,但我会提供另一种选择。这是一个微不足道的线性代数问题,我介绍的方法适用于平移,缩放,倾斜等情况。也就是说,如果图像已经转换,则效果很好。

缩放矩阵时,缩放比例在点(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


1
我必须说,如果您可以使用仿射变换矩阵,请热情地使用它。许多变换矩阵甚至都具有zoom(sx,sy,x,y)函数,可以做到这一点。如果您不使用它,几乎值得一煮。
塔塔兹化

实际上,我承认在此解决方案中使用的代码中,此后已将其替换为矩阵类。而且我已经多次做了这个确切的事情,并且已经编写了不少于两次的矩阵类。(github.com/EmbroidePy/pyembroidery/blob/master/pyembroidery/...),(github.com/EmbroidePy/EmbroidePy/blob/master/embroidepy/...)。如果您需要比这些运算更复杂的东西,那么矩阵基本上是正确的答案,一旦掌握了线性代数,您就会意识到该答案实际上是最佳答案。
12/17

6

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

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>


4

这是使用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-翻译)/比例= canvasCoords


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

    }


})

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

2

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

$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

在缩放之前和之后,您需要获取世界空间中的点(与屏幕空间相对),然后按增量进行平移。

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

0

您可以使用scrollto(x,y)函数将滚动条的位置处理到缩放后需要显示的位置。要找到鼠标的位置,请使用event.clientX和event.clientY。 这会帮助你


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.