jQuery 使用 Hammer.js 捏合缩放

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/18011099/
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

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-26 20:21:32  来源:igfitidea点击:

Pinch to zoom using Hammer.js

javascriptjquerypinchzoomhammer.js

提问by nickalchemist

I am trying to implement pinch to zoom using hammer.js

我正在尝试使用hammer.js实现捏合缩放

Here's my HTML-

这是我的 HTML-

 <script src="//cdnjs.cloudflare.com/ajax/libs/hammer.js/1.0.5/hammer.min.js"></script>

<div id="pinchzoom">
        <div>
            <img id="rect" src="http://blog.gettyimages.com/wp-content/uploads/2013/01/Siberian-Tiger-Running-Through-Snow-Tom-Brakefield-Getty-Images-200353826-001.jpg" width="2835" height="4289" ondragstart="return false" alt="" />
        </div>
    </div>

Here's my SCRIPT

这是我的脚本

var hammertime = Hammer(document.getElementById('pinchzoom'), {
        transform_always_block: true,
        transform_min_scale: 1,
        drag_block_horizontal: false,
        drag_block_vertical: false,
        drag_min_distance: 0
    });

    var rect = document.getElementById('rect');

    var posX=0, posY=0,
        scale=1, last_scale,
        rotation= 1, last_rotation;

    hammertime.on('touch drag transform', function(ev) {
        switch(ev.type) {
            case 'touch':
                last_scale = scale;
                last_rotation = rotation;
                break;

            case 'drag':
                posX = ev.gesture.deltaX;
                posY = ev.gesture.deltaY;
                break;

            case 'transform':
                rotation = last_rotation + ev.gesture.rotation;
                scale = Math.max(1, Math.min(last_scale * ev.gesture.scale, 10));
                break;
        }

        // transform!
        var transform =
                //"translate3d("+posX+"px,"+posY+"px, 0) " +
                "scale3d("+scale+","+scale+", 0) ";


        rect.style.transform = transform;
        rect.style.oTransform = transform;
        rect.style.msTransform = transform;
        rect.style.mozTransform = transform;
        rect.style.webkitTransform = transform;
    });

It works fine but I am not able to scroll the image. On uncommenting transform3d it works but image looses its position on drag. I can't use jQuery.

它工作正常,但我无法滚动图像。在取消注释 transform3d 时,它可以工作,但图像在拖动时会丢失其位置。我不能使用 jQuery。

回答by K'shin Gendron

For hammer.js2.0+ I have taken @DGS answer and changed it to suit what I was doing with cordovaand intel-xdkso it's pure JS and hammer.js2.0 for webkit.

对于hammer.js2.0+,我采用了@DGS 答案并将其更改为适合我使用cordovaintel-xdk 所做的工作,因此它是纯JS 和用于webkit 的hammer.js2.0 。

My implementation allows you to zoom and drag at the same time, not independent of each other as above, and provides for a more native feel. It also implements the double tap to zoom in (and to zoom back out). I have it set to zoom between .999and 4, but you can do as you like just changing those values. So if you just copy and paste this it will probably do what you expect it to (on webkit).

我的实现允许您同时缩放和拖动,而不是像上面那样相互独立,并提供更原生的感觉。它还实现了双击放大(和缩小)。我已将其设置为在.999和之间缩放4,但您可以随心所欲地更改这些值。因此,如果您只是复制并粘贴它,它可能会按照您的预期执行(webkit 上)。

Thanks to Eight Media and @DGS for getting me started! Feel free to improve it SO.

感谢八媒体和@DGS 让我开始!随意改进它。

function hammerIt(elm) {
    hammertime = new Hammer(elm, {});
    hammertime.get('pinch').set({
        enable: true
    });
    var posX = 0,
        posY = 0,
        scale = 1,
        last_scale = 1,
        last_posX = 0,
        last_posY = 0,
        max_pos_x = 0,
        max_pos_y = 0,
        transform = "",
        el = elm;

    hammertime.on('doubletap pan pinch panend pinchend', function(ev) {
        if (ev.type == "doubletap") {
            transform =
                "translate3d(0, 0, 0) " +
                "scale3d(2, 2, 1) ";
            scale = 2;
            last_scale = 2;
            try {
                if (window.getComputedStyle(el, null).getPropertyValue('-webkit-transform').toString() != "matrix(1, 0, 0, 1, 0, 0)") {
                    transform =
                        "translate3d(0, 0, 0) " +
                        "scale3d(1, 1, 1) ";
                    scale = 1;
                    last_scale = 1;
                }
            } catch (err) {}
            el.style.webkitTransform = transform;
            transform = "";
        }

        //pan    
        if (scale != 1) {
            posX = last_posX + ev.deltaX;
            posY = last_posY + ev.deltaY;
            max_pos_x = Math.ceil((scale - 1) * el.clientWidth / 2);
            max_pos_y = Math.ceil((scale - 1) * el.clientHeight / 2);
            if (posX > max_pos_x) {
                posX = max_pos_x;
            }
            if (posX < -max_pos_x) {
                posX = -max_pos_x;
            }
            if (posY > max_pos_y) {
                posY = max_pos_y;
            }
            if (posY < -max_pos_y) {
                posY = -max_pos_y;
            }
        }


        //pinch
        if (ev.type == "pinch") {
            scale = Math.max(.999, Math.min(last_scale * (ev.scale), 4));
        }
        if(ev.type == "pinchend"){last_scale = scale;}

        //panend
        if(ev.type == "panend"){
            last_posX = posX < max_pos_x ? posX : max_pos_x;
            last_posY = posY < max_pos_y ? posY : max_pos_y;
        }

        if (scale != 1) {
            transform =
                "translate3d(" + posX + "px," + posY + "px, 0) " +
                "scale3d(" + scale + ", " + scale + ", 1)";
        }

        if (transform) {
            el.style.webkitTransform = transform;
        }
    });
}

To implement just call it with hammerIt(document.getElementById("imagid")); after the element has loaded. You can call this on as many elements as you like.

要实现只需使用hammerIt(document.getElementById("imagid")); 元素加载后。您可以根据需要在任意数量的元素上调用它。

Cheers!

干杯!

回答by DGS

Add variables last_posXand last_posYto account for the change in position when you have translated. update them on dragend so that next time a drag event is captured it takes into account where the last drag ended

添加变量last_posXlast_posY说明翻译时位置的变化。在 dragend 上更新它们,以便下次捕获拖动事件时考虑上次拖动结束的位置

var posX=0, posY=0,
    scale=1, last_scale,
    last_posX=0, last_posY=0,
    max_pos_x=0, max_pos_y=0;

hammertime.on('touch drag transform dragend', function(ev) {
    switch(ev.type) {
        case 'touch':
            last_scale = scale;
            break;

        case 'drag':
            if(scale != 1){
                    posX = last_posX + ev.gesture.deltaX;
                    posY = last_posY + ev.gesture.deltaY;
                    if(posX > max_pos_x){
                        posX = max_pos_x;
                    }
                    if(posX < -max_pos_x){
                        posX = -max_pos_x;
                    }
                    if(posY > max_pos_y){
                        posY = max_pos_y;
                    }
                    if(posY < -max_pos_y){
                        posY = -max_pos_y;
                    }
            }else{
                posX = 0;
                posY = 0;
                saved_posX = 0;
                saved_posY = 0;
            }
            break;

        case 'transform':
            scale = Math.max(1, Math.min(last_scale * ev.gesture.scale, 10));
            max_pos_x = Math.ceil((scale - 1) * rect.clientWidth / 2);
            max_pos_y = Math.ceil((scale - 1) * rect.clientHeight / 2);
            if(posX > max_pos_x){
                posX = max_pos_x;
            }
            if(posX < -max_pos_x){
                posX = -max_pos_x;
            }
            if(posY > max_pos_y){
                posY = max_pos_y;
            }
            if(posY < -max_pos_y){
                posY = -max_pos_y;
            }
            break;
        case 'dragend':
            last_posX = posX < max_pos_x ? posX: max_pos_x;
            last_posY = posY < max_pos_y ? posY: max_pos_y;
            break;
    }

    // transform!
    var transform =
            "translate3d(0, 0, 0) " +
            "scale3d(1, 1, 0) "; 
    if(scale != 1){
        transform =
            "translate3d("+posX+"px,"+posY+"px, 0) " +
            "scale3d("+scale+","+scale+", 0) ";
    }

    rect.style.transform = transform;
    rect.style.oTransform = transform;
    rect.style.msTransform = transform;
    rect.style.mozTransform = transform;
    rect.style.webkitTransform = transform;
});

回答by st_phan

I wasn't really satisfied with the solutions so I created one too.

我对这些解决方案并不满意,所以我也创建了一个。

You can play with it at https://bl.ocks.org/stephanbogner/06c3e0d3a1c8fcca61b5345e1af80798

你可以在https://bl.ocks.org/stephanbogner/06c3e0d3a1c8fcca61b5345e1af80798玩它

The code is more complex, but it behaves like on iOS (tested on iPad and iPhone):

代码更复杂,但它的行为就像在 iOS 上一样(在 iPad 和 iPhone 上测试):

  • You double-tap onto the image and it zooms into that exact location
  • While pinching the zoom center is between your fingers and you can drag while pinching
  • 您双击图像,它会放大到该确切位置
  • 捏合缩放中心在您的手指之间,您可以在捏合时拖动


<!DOCTYPE html>
<html> 
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>HammerJS Pinch Zoom and Drag</title>
    <style type="text/css">
    body{
        margin: 0;
        padding: 0;
    }
    #dragWrapper{
        margin: 40px;
        background: whitesmoke;
        width: calc(100vw - 80px);
        height: calc(100vh - 80px);
        position: relative;
    }
    #drag {
        touch-action: auto;
        height: 300px;
        width: 200px;
        position: absolute;
        left: 0;
        top: 0;
        background-image: url('https://upload.wikimedia.org/wikipedia/commons/thumb/4/4a/Bergelut_dengan_asap_nan_beracun.jpg/318px-Bergelut_dengan_asap_nan_beracun.jpg');
            background-size: cover;
    }
    </style>
</head>
<body>
    <div id="dragWrapper">
        <div id="drag"></div>
    </div>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js"></script>
    <script type="text/javascript">
    var element = document.getElementById('drag')
    var hammertime = new Hammer(element, {});
    hammertime.get('pinch').set({ enable: true });
    hammertime.get('pan').set({ threshold: 0 });
    var fixHammerjsDeltaIssue = undefined;
    var pinchStart = { x: undefined, y: undefined }
    var lastEvent = undefined;
    var originalSize = {
        width: 200,
        height: 300
    }
    var current = {
        x: 0,
        y: 0,
        z: 1,
        zooming: false,
        width: originalSize.width * 1,
        height: originalSize.height * 1,
    }
    var last = {
        x: current.x,
        y: current.y,
        z: current.z
    }
    function getRelativePosition(element, point, originalSize, scale) {
        var domCoords = getCoords(element);
        var elementX = point.x - domCoords.x;
        var elementY = point.y - domCoords.y;
        var relativeX = elementX / (originalSize.width * scale / 2) - 1;
        var relativeY = elementY / (originalSize.height * scale / 2) - 1;
        return { x: relativeX, y: relativeY }
    }
    function getCoords(elem) { // crossbrowser version
        var box = elem.getBoundingClientRect();
        var body = document.body;
        var docEl = document.documentElement;
        var scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop;
        var scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft;
        var clientTop = docEl.clientTop || body.clientTop || 0;
        var clientLeft = docEl.clientLeft || body.clientLeft || 0;
        var top  = box.top +  scrollTop - clientTop;
        var left = box.left + scrollLeft - clientLeft;
        return { x: Math.round(left), y: Math.round(top) };
    }
    function scaleFrom(zoomOrigin, currentScale, newScale) {
        var currentShift = getCoordinateShiftDueToScale(originalSize, currentScale);
        var newShift = getCoordinateShiftDueToScale(originalSize, newScale)
        var zoomDistance = newScale - currentScale

        var shift = {
            x: currentShift.x - newShift.x,
            y: currentShift.y - newShift.y,
        }
        var output = {
            x: zoomOrigin.x * shift.x,
            y: zoomOrigin.y * shift.y,
            z: zoomDistance
        }
        return output
    }
    function getCoordinateShiftDueToScale(size, scale){
        var newWidth = scale * size.width;
        var newHeight = scale * size.height;
        var dx = (newWidth - size.width) / 2
        var dy = (newHeight - size.height) / 2
        return {
            x: dx,
            y: dy
        }
    }
    hammertime.on('doubletap', function(e) {
        var scaleFactor = 1;
        if (current.zooming === false) {
            current.zooming = true;
        } else {
            current.zooming = false;
            scaleFactor = -scaleFactor;
        }
        element.style.transition = "0.3s";
        setTimeout(function() {
            element.style.transition = "none";
        }, 300)
        var zoomOrigin = getRelativePosition(element, { x: e.center.x, y: e.center.y }, originalSize, current.z);
        var d = scaleFrom(zoomOrigin, current.z, current.z + scaleFactor)
        current.x += d.x;
        current.y += d.y;
        current.z += d.z;
        last.x = current.x;
        last.y = current.y;
        last.z = current.z;
        update();
    })
    hammertime.on('pan', function(e) {
        if (lastEvent !== 'pan') {
            fixHammerjsDeltaIssue = {
                x: e.deltaX,
                y: e.deltaY
            }
        }
        current.x = last.x + e.deltaX - fixHammerjsDeltaIssue.x;
        current.y = last.y + e.deltaY - fixHammerjsDeltaIssue.y;
        lastEvent = 'pan';
        update();
    })    
    hammertime.on('pinch', function(e) {
        var d = scaleFrom(pinchZoomOrigin, last.z, last.z * e.scale)
        current.x = d.x + last.x + e.deltaX;
        current.y = d.y + last.y + e.deltaY;
        current.z = d.z + last.z;
        lastEvent = 'pinch';
        update();
    })
    var pinchZoomOrigin = undefined;
    hammertime.on('pinchstart', function(e) {
        pinchStart.x = e.center.x;
        pinchStart.y = e.center.y;
        pinchZoomOrigin = getRelativePosition(element, { x: pinchStart.x, y: pinchStart.y }, originalSize, current.z);
        lastEvent = 'pinchstart';
    })
    hammertime.on('panend', function(e) {
        last.x = current.x;
        last.y = current.y;
        lastEvent = 'panend';
    })
    hammertime.on('pinchend', function(e) {
        last.x = current.x;
        last.y = current.y;
        last.z = current.z;
        lastEvent = 'pinchend';
    })
    function update() {
        current.height = originalSize.height * current.z;
        current.width = originalSize.width * current.z;
        element.style.transform = "translate3d(" + current.x + "px, " + current.y + "px, 0) scale(" + current.z + ")";
    }
    </script>
</body>

</html>

回答by redgeoff

Check out the Pinch Zoom and Pan with HammerJS demo. This example has been tested on Android, iOS and Windows Phone.

查看带有 HammerJS捏缩放和平移演示。此示例已在 Android、iOS 和 Windows Phone 上进行了测试。

You can find the source code at Pinch Zoom and Pan with HammerJS.

您可以在Pinch Zoom and Pan with HammerJS 中找到源代码。

For your convenience, here is the source code:

为了您的方便,这里是源代码:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport"
        content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
  <title>Pinch Zoom</title>
</head>

<body>

  <div>

    <div style="height:150px;background-color:#eeeeee">
      Ignore this area. Space is needed to test on the iPhone simulator as pinch simulation on the
      iPhone simulator requires the target to be near the middle of the screen and we only respect
      touch events in the image area. This space is not needed in production.
    </div>

    <style>

      .pinch-zoom-container {
        overflow: hidden;
        height: 300px;
      }

      .pinch-zoom-image {
        width: 100%;
      }

    </style>

    <script src="https://hammerjs.github.io/dist/hammer.js"></script>

    <script>

      var MIN_SCALE = 1; // 1=scaling when first loaded
      var MAX_SCALE = 64;

      // HammerJS fires "pinch" and "pan" events that are cumulative in nature and not
      // deltas. Therefore, we need to store the "last" values of scale, x and y so that we can
      // adjust the UI accordingly. It isn't until the "pinchend" and "panend" events are received
      // that we can set the "last" values.

      // Our "raw" coordinates are not scaled. This allows us to only have to modify our stored
      // coordinates when the UI is updated. It also simplifies our calculations as these
      // coordinates are without respect to the current scale.

      var imgWidth = null;
      var imgHeight = null;
      var viewportWidth = null;
      var viewportHeight = null;
      var scale = null;
      var lastScale = null;
      var container = null;
      var img = null;
      var x = 0;
      var lastX = 0;
      var y = 0;
      var lastY = 0;
      var pinchCenter = null;

      // We need to disable the following event handlers so that the browser doesn't try to
      // automatically handle our image drag gestures.
      var disableImgEventHandlers = function () {
        var events = ['onclick', 'onmousedown', 'onmousemove', 'onmouseout', 'onmouseover',
                      'onmouseup', 'ondblclick', 'onfocus', 'onblur'];

        events.forEach(function (event) {
          img[event] = function () {
            return false;
          };
        });
      };

      // Traverse the DOM to calculate the absolute position of an element
      var absolutePosition = function (el) {
        var x = 0,
          y = 0;

        while (el !== null) {
          x += el.offsetLeft;
          y += el.offsetTop;
          el = el.offsetParent;
        }

        return { x: x, y: y };
      };

      var restrictScale = function (scale) {
        if (scale < MIN_SCALE) {
          scale = MIN_SCALE;
        } else if (scale > MAX_SCALE) {
          scale = MAX_SCALE;
        }
        return scale;
      };

      var restrictRawPos = function (pos, viewportDim, imgDim) {
        if (pos < viewportDim/scale - imgDim) { // too far left/up?
          pos = viewportDim/scale - imgDim;
        } else if (pos > 0) { // too far right/down?
          pos = 0;
        }
        return pos;
      };

      var updateLastPos = function (deltaX, deltaY) {
        lastX = x;
        lastY = y;
      };

      var translate = function (deltaX, deltaY) {
        // We restrict to the min of the viewport width/height or current width/height as the
        // current width/height may be smaller than the viewport width/height

        var newX = restrictRawPos(lastX + deltaX/scale,
                                  Math.min(viewportWidth, curWidth), imgWidth);
        x = newX;
        img.style.marginLeft = Math.ceil(newX*scale) + 'px';

        var newY = restrictRawPos(lastY + deltaY/scale,
                                  Math.min(viewportHeight, curHeight), imgHeight);
        y = newY;
        img.style.marginTop = Math.ceil(newY*scale) + 'px';
      };

      var zoom = function (scaleBy) {
        scale = restrictScale(lastScale*scaleBy);

        curWidth = imgWidth*scale;
        curHeight = imgHeight*scale;

        img.style.width = Math.ceil(curWidth) + 'px';
        img.style.height = Math.ceil(curHeight) + 'px';

        // Adjust margins to make sure that we aren't out of bounds
        translate(0, 0);
      };

      var rawCenter = function (e) {
        var pos = absolutePosition(container);

        // We need to account for the scroll position
        var scrollLeft = window.pageXOffset ? window.pageXOffset : document.body.scrollLeft;
        var scrollTop = window.pageYOffset ? window.pageYOffset : document.body.scrollTop;

        var zoomX = -x + (e.center.x - pos.x + scrollLeft)/scale;
        var zoomY = -y + (e.center.y - pos.y + scrollTop)/scale;

        return { x: zoomX, y: zoomY };
      };

      var updateLastScale = function () {
        lastScale = scale;
      };

      var zoomAround = function (scaleBy, rawZoomX, rawZoomY, doNotUpdateLast) {
        // Zoom
        zoom(scaleBy);

        // New raw center of viewport
        var rawCenterX = -x + Math.min(viewportWidth, curWidth)/2/scale;
        var rawCenterY = -y + Math.min(viewportHeight, curHeight)/2/scale;

        // Delta
        var deltaX = (rawCenterX - rawZoomX)*scale;
        var deltaY = (rawCenterY - rawZoomY)*scale;

        // Translate back to zoom center
        translate(deltaX, deltaY);

        if (!doNotUpdateLast) {
          updateLastScale();
          updateLastPos();
        }
      };

      var zoomCenter = function (scaleBy) {
        // Center of viewport
        var zoomX = -x + Math.min(viewportWidth, curWidth)/2/scale;
        var zoomY = -y + Math.min(viewportHeight, curHeight)/2/scale;

        zoomAround(scaleBy, zoomX, zoomY);
      };

      var zoomIn = function () {
        zoomCenter(2);
      };

      var zoomOut = function () {
        zoomCenter(1/2);
      };

      var onLoad = function () {

        img = document.getElementById('pinch-zoom-image-id');
        container = img.parentElement;

        disableImgEventHandlers();

        imgWidth = img.width;
        imgHeight = img.height;
        viewportWidth = img.offsetWidth;
        scale = viewportWidth/imgWidth;
        lastScale = scale;
        viewportHeight = img.parentElement.offsetHeight;
        curWidth = imgWidth*scale;
        curHeight = imgHeight*scale;

        var hammer = new Hammer(container, {
          domEvents: true
        });

        hammer.get('pinch').set({
          enable: true
        });

        hammer.on('pan', function (e) {
          translate(e.deltaX, e.deltaY);
        });

        hammer.on('panend', function (e) {
          updateLastPos();
        });

        hammer.on('pinch', function (e) {

          // We only calculate the pinch center on the first pinch event as we want the center to
          // stay consistent during the entire pinch
          if (pinchCenter === null) {
            pinchCenter = rawCenter(e);
            var offsetX = pinchCenter.x*scale - (-x*scale + Math.min(viewportWidth, curWidth)/2);
            var offsetY = pinchCenter.y*scale - (-y*scale + Math.min(viewportHeight, curHeight)/2);
            pinchCenterOffset = { x: offsetX, y: offsetY };
          }

          // When the user pinch zooms, she/he expects the pinch center to remain in the same
          // relative location of the screen. To achieve this, the raw zoom center is calculated by
          // first storing the pinch center and the scaled offset to the current center of the
          // image. The new scale is then used to calculate the zoom center. This has the effect of
          // actually translating the zoom center on each pinch zoom event.
          var newScale = restrictScale(scale*e.scale);
          var zoomX = pinchCenter.x*newScale - pinchCenterOffset.x;
          var zoomY = pinchCenter.y*newScale - pinchCenterOffset.y;
          var zoomCenter = { x: zoomX/newScale, y: zoomY/newScale };

          zoomAround(e.scale, zoomCenter.x, zoomCenter.y, true);
        });

        hammer.on('pinchend', function (e) {
          updateLastScale();
          updateLastPos();
          pinchCenter = null;
        });

        hammer.on('doubletap', function (e) {
          var c = rawCenter(e);
          zoomAround(2, c.x, c.y);
        });

      };

    </script>

    <button onclick="zoomIn()">Zoom In</button>
    <button onclick="zoomOut()">Zoom Out</button>

    <div class="pinch-zoom-container">
      <img id="pinch-zoom-image-id" class="pinch-zoom-image" onload="onLoad()"
           src="https://hammerjs.github.io/assets/img/pano-1.jpg">
    </div>


  </div>

</body>
</html>

(BTW, There is a lot of good info above, but I couldn't get any of the examples to actually work and there are some subtle details that I wanted to implement such as consideration of the zoom center).

(顺便说一句,上面有很多很好的信息,但我无法让任何示例实际工作,并且我想实现一些微妙的细节,例如考虑缩放中心)。