41

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

Here's my 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.

nickalchemist
  • 2,211
  • 6
  • 31
  • 58

5 Answers5

66

For 2.0+ I have taken @DGS answer and changed it to suit what I was doing with and so it's pure JS and 2.0 for .

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

Thanks to Eight Media and @DGS for getting me started! Feel free to improve it 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;
        }
    });
}

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.

Cheers!

Paul T. Rawkeen
  • 3,994
  • 3
  • 35
  • 51
K'shin Gendron
  • 1,589
  • 1
  • 12
  • 14
  • This is great, but I'm a little confused with the tests for 'scale != 1'. My scale is always 1 unless I've started to pinch, meaning that those gestures get skipped. – Jason Farnsworth Feb 01 '15 at 06:41
  • 1
    @JasonFarnsworth You can't pan/drag an image that hasn't been zoomed in on, so there's no point in running that code unless zoom is > 1. – K'shin Gendron Feb 03 '15 at 01:07
  • Ah, got it. I'm using a different approach -- I let the user pan, and then I transition back to a reasonable state if they go out of bounds. – Jason Farnsworth Feb 03 '15 at 02:36
  • thnkas, zoom is working :) , but pan is not working properly :( – Naveen Prince P May 25 '16 at 11:21
  • Why are you using 0.999 as a max value and not 1? With 0.999, after one zoom-in and total zoom-out the scale reaches 0.999, when should be 1, as the original value. – glerendegui May 12 '17 at 20:01
  • This works great, the only problem is that the swipeup and swipedown events are ignored so a user can not scroll a page when touch is over an elm. – Mike Purcell Oct 25 '17 at 20:30
  • How would I modify this code (or execute as-is) to restrict all transformations to 2 finger touches? As it stands, I cannot scroll with one finger down a page that includes a full-screen-width image under the control of the routine. Screen scrolling stops completely, and can only be activated again by swiping above or below the image, which isn't intuitive. Instead, if all transformations of the image required 2 "touches" (i.e., simultaneous pressings of two fingers), then screen scrolling would not be thwarted/interrupted, and all zooming/panning/etc. would be possible with 2-touch actions. – Tom Oct 06 '18 at 03:46
15

Add variables last_posX and last_posY to 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

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;
});
DGS
  • 6,015
  • 1
  • 21
  • 37
  • @DGS- I want to retain original image size i.e. whenever the scale is 1 or less image should get to it's original size and position on pinching in. – nickalchemist Aug 17 '13 at 09:33
  • 1
    @Nickalchemist updated answer. Now if the scale is 1 or less the css translation and scale will reset to default. – DGS Aug 17 '13 at 10:35
  • @DGS- Position is still not fixed. Am still able to drag image on 0 scale. – nickalchemist Aug 19 '13 at 11:41
  • @DGS- When I zoom and drag I am still able to see white space. This is not how a typical pinch to zoom works. There should be no whitespace. – nickalchemist Aug 21 '13 at 09:19
  • So you want to not be able to scroll beyond the limits of the image is what you are saying? – DGS Aug 21 '13 at 09:22
  • Updated code tell me if that works i haven't tested it but the concept is there – DGS Aug 21 '13 at 09:40
  • Updated code have tested it and appears to do everyone you want it to do – DGS Aug 21 '13 at 11:05
  • @DGS- I am still able to drag image and see whitespace. – nickalchemist Aug 21 '13 at 12:38
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/35932/discussion-between-nickalchemist-and-dgs) – nickalchemist Aug 21 '13 at 16:30
  • How can you zoom out with this code? It seems it's restricted to a minimum of 1 scale. – M Rajoy Sep 17 '13 at 07:48
  • You cant. The idea was to implement pinch to zoom which generally gives you a base zoom level and allows you to go closer if you wish. – DGS Sep 17 '13 at 08:06
  • Is it possible that there is a confusion between saved_posX and last_posX? I don't see saved_posX ever set. Should these be combined into one variable? – avprod1 Nov 01 '13 at 16:20
  • @DGS i'm using your solution in a web app build with Cordova for Android and iOS. In Android is working well but in iOS it doesn't work. I think this happen because iOS is trying to execute some native events. Do you know how to prevent this? Thanks – Bellu Aug 02 '14 at 11:24
  • @Bellu is there something in particular not working in iOS or are you unable to drag and zoom altogether. I have implemented this solution or something similar to it in a Cordova application, though it was an older version so some changes may have been made. – DGS Aug 04 '14 at 04:45
  • @DGS any chance for you to have updated version of this code? With hammertime.get('pinch').set({ enable: true }); – bamya Jan 29 '19 at 01:41
10

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

The code is more complex, but it behaves like on iOS (tested on iPad and 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>
st_phan
  • 715
  • 9
  • 23
  • I would really like to try it out, but the link is not accessible – goldenriver4422 Dec 13 '22 at 01:37
  • 1
    @goldenriver4422 I'd assume bl.ocks.org will be online soon, but feel free to take my code above save it into a "index.html"-file and host it on a server. Or you can also [start a localhost](https://chrome.google.com/webstore/detail/web-server-for-chrome/ofhbbkphhbklhfoeikjpcbhemlocgigb) and [view it on your mobile device](https://blog.bitsrc.io/how-to-view-localhost-web-apps-on-mobile-browsers-2b7433df4abd). – st_phan Dec 14 '22 at 04:52
3

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

You can find the source code at 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).

redgeoff
  • 3,163
  • 1
  • 25
  • 39
  • There is a small bug in onLoad() function. "viewportWidth = img.offsetWidth;" should be viewportWidth = img.parentElement offsetWidth;" – lhrec_106 Oct 18 '16 at 00:17
1

Here's a relatively compact solution using paper.js (codepen):

function initPinchZoom() {
    const canvasElement = paper.view.element;
    const box = canvasElement.getBoundingClientRect();
    const offset = new paper.Point(box.left, box.top);

    const hammer = new Hammer(canvasElement, {});
    hammer.get('pinch').set({ enable: true });

    let startMatrix, startMatrixInverted, p0ProjectCoords;

    hammer.on('pinchstart', e => {
        startMatrix = paper.view.matrix.clone();
        startMatrixInverted = startMatrix.inverted();
        const p0 = getCenterPoint(e);
        p0ProjectCoords = paper.view.viewToProject(p0);
    });

    hammer.on('pinch', e => {
        // Translate and scale view using pinch event's 'center' and 'scale' properties.
        // Translation computes center's distance from initial center (considering current scale).
        const p = getCenterPoint(e);
        const pProject0 = p.transform(startMatrixInverted);
        const delta = pProject0.subtract(p0ProjectCoords).divide(e.scale);
        paper.view.matrix = startMatrix.clone().scale(e.scale, p0ProjectCoords).translate(delta);
    });

    function getCenterPoint(e) {
        return new paper.Point(e.center.x, e.center.y).subtract(offset);
    }
}
Rick Mohr
  • 1,821
  • 1
  • 19
  • 22