简体   繁体   English

CSS3 缩放鼠标光标

[英]CSS3 zooming on mouse cursor

My goal is to create a plugin that enables zooming & panning operations on a page area, just like how Google Maps currently works (meaning: scrolling with the mouse = zooming in/out of the area, click & hold & move & release = panning).我的目标是创建一个插件,可以在页面区域上启用缩放和平移操作,就像 Google 地图目前的工作方式一样(意思是:用鼠标滚动 = 放大/缩小区域,单击并按住并移动并释放 = 平移).

When scrolling, I wish to have a zoom operation centered on the mouse cursor.滚动时,我希望以鼠标光标为中心进行缩放操作。

For this, I use on-the-fly CSS3 matrix transformations.为此,我使用了即时 CSS3 矩阵转换。 The only, yet mandatory, constraint is that I cannot use anything else than CSS3 translate & scale transformations, with a transform origin of 0px 0px.唯一但强制性的约束是我不能使用 CSS3 平移和缩放转换以外的任何东西,转换原点为 0px 0px。

Panning is out of the scope of my question, since I have it working already.平移不在我的问题范围内,因为我已经在使用它了。 When it comes to zooming, I am struggling to figure out where the glitch is in my Javascript code.说到缩放,我很难找出我的 Javascript 代码中的错误所在。

The problem must be somewhere in the MouseZoom.prototype.zoom function, in the calculation of the translation on the x axis and y axis.问题一定出在MouseZoom.prototype.zoom函数的某处,在计算 x 轴和 y 轴上的平移时。

First, here is my HTML code:首先,这是我的 HTML 代码:

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width = device-width, initial-scale = 1.0, user-scalable = no" />
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black" />
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
    <script src="jquery.mousewheel.min.js"></script>
    <script src="StackOverflow.js"></script>
    <style type="text/css" media="all">
        #drawing {
            position: absolute;
            top: 0px; 
            left: 0px; 
            right:0; 
            bottom:0;
            z-index: 0;
            background: url(http://catmacros.files.wordpress.com/2009/09/cats_banzai.jpg) no-repeat;
            background-position: 50% 50%;
        }
    </style>
    <title>Test</title>
</head>
<body>
    <div id="drawing"></div>
    <script>
        var renderer = new ZoomPanRenderer("drawing");
    </script>
</body>
</html>

As you can see, I am using Jquery and the jquery mouse wheel plugin from Brandon Aaron, which can be found here: https://github.com/brandonaaron/jquery-mousewheel/如您所见,我正在使用 Jquery 和来自 Brandon Aaron 的 jquery 鼠标滚轮插件,可在此处找到: https ://github.com/brandonaaron/jquery-mousewheel/

Here is the content of the StackOverflow.js file:以下是 StackOverflow.js 文件的内容:

/***************************************************** 
 * Transformations
 ****************************************************/
function Transformations(translateX, translateY, scale){
    this.translateX = translateX;
    this.translateY = translateY;
    this.scale = scale;
}

/* Getters */
Transformations.prototype.getScale = function(){ return this.scale; }
Transformations.prototype.getTranslateX = function(){ return this.translateX; }
Transformations.prototype.getTranslateY = function(){ return this.translateY; }

/***************************************************** 
 * Zoom Pan Renderer
 ****************************************************/
function ZoomPanRenderer(elementId){
    this.zooming = undefined;
    this.elementId = elementId;
    this.current = new Transformations(0, 0, 1);
    this.last = new Transformations(0, 0, 1);
    new ZoomPanEventHandlers(this);
}

/* setters */
ZoomPanRenderer.prototype.setCurrentTransformations = function(t){ this.current = t; }
ZoomPanRenderer.prototype.setZooming = function(z){ this.zooming = z; }

/* getters */
ZoomPanRenderer.prototype.getCurrentTransformations = function(){ return this.current; }
ZoomPanRenderer.prototype.getZooming = function(){ return this.zooming; }
ZoomPanRenderer.prototype.getLastTransformations = function(){ return this.last; }
ZoomPanRenderer.prototype.getElementId = function(){ return this.elementId; }

/* Rendering */
ZoomPanRenderer.prototype.getTransform3d = function(t){
    var transform3d = "matrix3d(";
    transform3d+= t.getScale().toFixed(10) + ",0,0,0,";
    transform3d+= "0," + t.getScale().toFixed(10) + ",0,0,";
    transform3d+= "0,0,1,0,";
    transform3d+= t.getTranslateX().toFixed(10) + "," + t.getTranslateY().toFixed(10)  + ",0,1)";
    return transform3d;
}

ZoomPanRenderer.prototype.getTransform2d = function(t){
    var transform3d = "matrix(";
    transform3d+= t.getScale().toFixed(10) + ",0,0," + t.getScale().toFixed(10) + "," + t.getTranslateX().toFixed(10) + "," + t.getTranslateY().toFixed(10) + ")";
    return transform3d;
}

ZoomPanRenderer.prototype.applyTransformations = function(t){
    var elem = $("#" + this.getElementId());
    elem.css("transform-origin", "0px 0px");
    elem.css("-ms-transform-origin", "0px 0px");
    elem.css("-o-transform-origin", "0px 0px");
    elem.css("-moz-transform-origin", "0px 0px");
    elem.css("-webkit-transform-origin", "0px 0px");
    var transform2d = this.getTransform2d(t);
    elem.css("transform", transform2d);
    elem.css("-ms-transform", transform2d);
    elem.css("-o-transform", transform2d);
    elem.css("-moz-transform", transform2d);
    elem.css("-webkit-transform", this.getTransform3d(t));
}

/***************************************************** 
 * Event handler
 ****************************************************/
function ZoomPanEventHandlers(renderer){
    this.renderer = renderer;
    
    /* Disable scroll overflow - safari */
    document.addEventListener('touchmove', function(e) { e.preventDefault(); }, false);
    
    /* Disable default drag opeartions on the element (FF makes it ready for save)*/
    $("#" + renderer.getElementId()).bind('dragstart', function(e) { e.preventDefault(); });
    
    /* Add mouse wheel handler */
    $("#" + renderer.getElementId()).bind("mousewheel", function(event, delta) {
        if(renderer.getZooming()==undefined){
            var offsetLeft = $("#" + renderer.getElementId()).offset().left;
            var offsetTop = $("#" + renderer.getElementId()).offset().top;
            var zooming = new MouseZoom(renderer.getCurrentTransformations(), event.pageX, event.pageY, offsetLeft, offsetTop, delta);
            renderer.setZooming(zooming);
            
            var newTransformation = zooming.zoom();
            renderer.applyTransformations(newTransformation);
            renderer.setCurrentTransformations(newTransformation);
            renderer.setZooming(undefined);
        }
        return false;
    });
}

/***************************************************** 
 * Mouse zoom
 ****************************************************/
function MouseZoom(t, mouseX, mouseY, offsetLeft, offsetTop, delta){
    this.current = t;
    this.offsetLeft = offsetLeft;
    this.offsetTop = offsetTop;
    this.mouseX = mouseX;
    this.mouseY = mouseY;
    this.delta = delta;
}

MouseZoom.prototype.zoom = function(){
    var previousScale = this.current.getScale();
    var newScale = previousScale + this.delta/5;
    if(newScale<1){
        newScale = 1;
    }
    var ratio = newScale / previousScale;
    
    var imageX = this.mouseX - this.offsetLeft;
    var imageY = this.mouseY - this.offsetTop;
    
    var previousTx = - this.current.getTranslateX() * previousScale;
    var previousTy = - this.current.getTranslateY() * previousScale;
    var previousDx = imageX * previousScale;
    var previousDy = imageY * previousScale;

    var newTx = (previousTx * ratio + previousDx * (ratio - 1)) / newScale;
    var newTy = (previousTy * ratio + previousDy * (ratio - 1)) / newScale;
    
    return new Transformations(-newTx, -newTy, newScale);
}

Using transform to get a google maps zooming behavior on a div element seemed like an interesting idea, so I payed with it a little =)使用transformdiv元素上获得谷歌地图缩放行为似乎是一个有趣的想法,所以我付了一点钱 =)

I would use transform-origin (and its sister attributes for browser compatibility) to adjust the zooming to the mouse location on the div that you are scaling.我会使用transform-origin (及其用于浏览器兼容性的姊妹属性)将缩放调整到您正在缩放的 div 上的鼠标位置。 I think this could do what you want.我认为这可以做你想做的。 I put some examples on fiddle for illustration:我在小提琴上放了一些例子来说明:

Adjusting the transform-origin调整transform-origin

So in the applyTransformations function of yours we could adjust the transform-origin dynamically from the imageX and imageY , if we pass this values from the MouseZoom (mouse listener) function.因此,在您的applyTransformations函数中,如果我们从MouseZoom (鼠标侦听器)函数传递此值,我们可以从imageXimageY动态调整transform-origin

    var orig = t.getTranslateX().toFixed() + "px " + t.getTranslateY().toFixed() + "px";
    elem.css("transform-origin", orig);
    elem.css("-ms-transform-origin", orig);
    elem.css("-o-transform-origin", orig);
    elem.css("-moz-transform-origin", orig);
    elem.css("-webkit-transform-origin", orig);

(In this first fiddle example I just used your translateX and translateY in Transformations to pass the location of the mouse on the div element - in the second example I renamed it to originX and originY to differentiate from the translation variables.) (在第一个小提琴示例中,我只是在Transformations中使用了translateXtranslateY来传递鼠标在 div 元素上的位置——在第二个示例中,我将其重命名为originXoriginY以区别于翻译变量。)

Calculating the transform origin计算变换原点

In your MouseZoom we can calculate origin location simply with imageX/previousScale .在您的MouseZoom ,我们可以简单地使用imageX/previousScale计算原点位置。

    MouseZoom.prototype.zoom = function(){
        var previousScale = this.current.getScale();
        var newScale = previousScale + this.delta/10;
        if(newScale<1){
            newScale = 1;
        }
        var ratio = newScale / previousScale;

        var imageX = this.mouseX - this.offsetLeft;
        var imageY = this.mouseY - this.offsetTop;

        var newTx = imageX/previousScale;
        var newTy = imageY/previousScale;

        return new Transformations(newTx, newTy, newScale);
    }

So this will work perfectly if you zoom out completely before zooming in on a different position.因此,如果您在放大到不同位置之前完全缩小,这将非常有效。 But to be able to change zoom origin at any zoom level, we can combine the origin and translation functionality.但是为了能够在任何缩放级别更改缩放原点,我们可以结合原点和平移功能。

Shifting the zooming frame (extending my original answer)移动缩放框(扩展我原来的答案)

The transform origin on the image is still calculated the same way but we use a separate translateX and translateY to shift the zooming frame (here I introduced two new variables that help us do the trick - so now we have originX , originY , translateX and translateY ).图像上的变换原点仍然以相同的方式计算,但我们使用单独的 translateX 和 translateY 来移动缩放框(这里我引入了两个新变量来帮助我们完成这个技巧 - 所以现在我们有originXoriginYtranslateXtranslateY ).

    MouseZoom.prototype.zoom = function(){
        // current scale
        var previousScale = this.current.getScale();
        // new scale
        var newScale = previousScale + this.delta/10;
        // scale limits
        var maxscale = 20;
        if(newScale<1){
            newScale = 1;
        }
        else if(newScale>maxscale){
            newScale = maxscale;
        }
        // current cursor position on image
        var imageX = (this.mouseX - this.offsetLeft).toFixed(2);
        var imageY = (this.mouseY - this.offsetTop).toFixed(2);
        // previous cursor position on image
        var prevOrigX = (this.current.getOriginX()*previousScale).toFixed(2);
        var prevOrigY = (this.current.getOriginY()*previousScale).toFixed(2);
        // previous zooming frame translate
        var translateX = this.current.getTranslateX();
        var translateY = this.current.getTranslateY();
        // set origin to current cursor position
        var newOrigX = imageX/previousScale;
        var newOrigY = imageY/previousScale;
        // move zooming frame to current cursor position
        if ((Math.abs(imageX-prevOrigX)>1 || Math.abs(imageY-prevOrigY)>1) && previousScale < maxscale) {
            translateX = translateX + (imageX-prevOrigX)*(1-1/previousScale);
            translateY = translateY + (imageY-prevOrigY)*(1-1/previousScale);
        }
        // stabilize position by zooming on previous cursor position
        else if(previousScale != 1 || imageX != prevOrigX && imageY != prevOrigY) {
            newOrigX = prevOrigX/previousScale;
            newOrigY = prevOrigY/previousScale;
        }
        return new Transformations(newOrigX, newOrigY, translateX, translateY, newScale);
    }

For this example I adjusted the your original script a little more and added the second fiddle example .对于这个例子,我稍微调整了你的原始脚本并添加了第二个小提琴示例

Now we zoom in and out on the mouse cursor from any zoom level.现在我们从任何缩放级别放大和缩小鼠标光标。 But because of the frame shift we end up moving the original div around ("measuring the earth")... which looks funny if you work with an object of limited width and hight (zoom-in at one end, zoom-out at another end, and we moved forward like an inchworm).但是由于帧偏移,我们最终移动了原始 div(“测量地球”)...如果您使用宽度和高度有限的对象(在一端放大,在另一端缩小),这看起来很有趣另一端,我们像尺蠖一样向前移动)。

Avoiding the "inchworm" effect避免“尺蠖”效应

To avoid this you could for example add limitations so that the left image border can not move to the right of its original x coordinate, the top image border can not move lower than its original y position, and so on for the other two borders.为避免这种情况,您可以添加限制,例如,左图像边框不能移动到其原始 x 坐标的右侧,顶部图像边框不能移动到其原始 y 位置以下,其他两个边框等等。 But then the zoom/out will not be completely bound to the cursor, but also by the edge of the image (you will notice the image slide into place) in example 3 .但是在示例 3中,缩放/缩小不会完全绑定到光标,还会绑定到图像的边缘(您会注意到图像滑入到位)。

    if(this.delta <= 0){
        var width = 500; // image width
        var height = 350; // image height
        if(translateX+newOrigX+(width - newOrigX)*newScale <= width){
            translateX = 0;
            newOrigX = width;
        }
        else if (translateX+newOrigX*(1-newScale) >= 0){
            translateX = 0;
            newOrigX = 0;        
        }
        if(translateY+newOrigY+(height - newOrigY)*newScale <= height){
            translateY = 0;
            newOrigY = height;
        }
        else if (translateY+newOrigY*(1-newScale) >= 0){
            translateY = 0;
            newOrigY = 0;
        }
    }

Another (a bit crappy) option would be to simply reset the frame translate when you zoom out completely (scale==1).另一个(有点蹩脚的)选项是在完全缩小时简单地重置帧转换(scale==1)。

However, you would not have this problem if you will be dealing with continuous elements (left and right edge and top and bottom edge bound together) or just with extremely big elements.但是,如果您要处理连续元素(左右边缘以及上下边缘绑定在一起)或仅处理非常大的元素,则不会遇到此问题。

To finish everything off with a nice touch - we can add a parent frame with hidden overflow around our scaling object.为了巧妙地完成所有事情——我们可以在我们的缩放对象周围添加一个带有隐藏溢出的父框架。 So the image area does not change with zooming.所以图像区域不会随着缩放而改变。 See jsfiddle example 4 .请参阅jsfiddle 示例 4

We made a react library for this: https://www.npmjs.com/package/react-map-interaction我们为此制作了一个反应库: https ://www.npmjs.com/package/react-map-interaction

It handles zooming and panning and works on both mobile and desktop.它处理缩放和平移,并可在移动设备和桌面设备上使用。

The source is fairly short and readable, but to answer your question here more directly, we use this CSS transform:源代码相当简短且易读,但为了更直接地回答您的问题,我们使用此 CSS 转换:

const transform = `translate(${translation.x}px, ${translation.y}px) scale(${scale})`;
const style = {
    transform: transform,
    transformOrigin: '0 0 '
};

// render the div with that style

One of the primary tricks is properly calculating the diff between the initial pointer/mouse down state and the current state when a touch/mouse move occurs.主要技巧之一是正确计算初始指针/鼠标按下状态与发生触摸/鼠标移动时的当前状态之间的差异。 When the mouse down occurs, capture the coordinates.当发生鼠标按下时,捕获坐标。 Then on every mouse move (until a mouse up) calculate the diff in the distance.然后在每次鼠标移动时(直到鼠标抬起)计算距离的差异。 That diff is what you need to offset the translation by in order to make sure the initial point under your cursor is the focal point of the zoom.该差异是您需要偏移平移的内容,以确保光标下的初始点是缩放的焦点。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM