web-dev-qa-db-ja.com

Hammer.jsを使用してピンチしてズームする

Hammer.jsを使用してズームするピンチを実装しようとしています

これが私の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>

これが私のスクリプトです

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

正常に動作しますが、画像をスクロールできません。 transform3dのコメントを外すと動作しますが、ドラッグすると画像の位置が失われます。 jQueryを使用できません。

32
nickalchemist

hammer.js 2.0+の場合、@ DGSの回答を取得し、 cordova および intel-xdk で行っていたことに合わせて変更しました。 pure JSおよび hammer.js 2.0 webkit の場合。

私の実装では、上記のように互いに独立しているのではなく、ズームとドラッグを同時に行うことができ、よりネイティブな感覚を提供します。また、ダブルタップを実装して、ズームイン()およびズームアウト)します。 .9994の間でズームするように設定していますが、これらの値を変更するだけで好きなようにできます。そのため、これをコピーして貼り付けるだけで、おそらく(on webkit)が期待どおりに動作します。

Eight Mediaと@DGSを始めてくれてありがとう! SOを自由に改善してください。

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

実装するには、単にhammerIt(document.getElementById( "imagid"))で呼び出します。要素がロードされた後。これを好きなだけの要素で呼び出すことができます。

乾杯!

46
K'shin Gendron

変数を追加last_posXおよびlast_posY翻訳したときの位置の変化を考慮します。ドラッグイベントがキャプチャされると、最後のドラッグが終了した場所が考慮されるように、ドラッグエンドでそれらを更新します。

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;
});
15
DGS

私はソリューションに本当に満足していなかったので、私もそれを作成しました。

https://bl.ocks.org/stephanbogner/06c3e0d3a1c8fcca61b5345e1af80798 でプレイできます

コードはより複雑ですが、iOS(iPadおよびiPhoneでテスト済み)のように動作します:

  • 画像をダブルタップすると、その正確な場所にズームします
  • ズームセンターをつまんでいる間は指の間にあり、つまんでいる間はドラッグできます

<!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>
7
st_phan

HammerJSデモでピンチズームとパン を確認してください。この例は、Android、iOS、およびWindows Phoneでテストされています。

ソースコードは HammerJSでピンチズームとパン で見つけることができます。

便宜上、ソースコードは次のとおりです。

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

(ところで、上にはたくさんの良い情報がありますが、実際に動作する例はありませんでした。ズームセンターの検討など、実装したい微妙な詳細があります)。

3
redgeoff