CSS3 zooming on mouse cursor

后端 未结 2 721
伪装坚强ぢ
伪装坚强ぢ 2020-12-13 10:51

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 = zo

相关标签:
2条回答
  • 2020-12-13 11:38

    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 =)

    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. I think this could do what you want. I put some examples on fiddle for illustration:

    • example 1: zoom in and out on transform-origin
    • example 2: zoom on transform-origin & shift zooming frame with translation
    • example 3: example 2 + zoom-out limited to original frame borders
    • example 4: example 3 + parent frame with hidden overflow

    Adjusting the 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.

        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.)

    Calculating the transform origin

    In your MouseZoom we can calculate origin location simply with 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).

        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).

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

        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).

    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.

    0 讨论(0)
  • 2020-12-13 11:42

    We made a react library for this: 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:

    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.

    0 讨论(0)
提交回复
热议问题