[javascript] Simplest way to detect a pinch

This is a WEB APP not a native app. Please no Objective-C NS commands.

So I need to detect 'pinch' events on iOS. Problem is every plugin or method I see for doing gestures or multi-touch events, is (usually) with jQuery and is a whole additional pluggin for every gesture under the sun. My application is huge, and I am very sensitive to deadwood in my code. All I need is to detect a pinch, and using something like jGesture is just way to bloated for my simple needs.

Additionally, I have a limited understanding of how to detect a pinch manually. I can get the position of both fingers, can't seem to get the mix right to detect this. Does anyone have a simple snippet that JUST detects pinch?

This question is related to javascript jquery ios mobile-safari

The answer is


None of these answers achieved what I was looking for, so I wound up writing something myself. I wanted to pinch-zoom an image on my website using my MacBookPro trackpad. The following code (which requires jQuery) seems to work in Chrome and Edge, at least. Maybe this will be of use to someone else.

function setupImageEnlargement(el)
{
    // "el" represents the image element, such as the results of document.getElementByd('image-id')
    var img = $(el);
    $(window, 'html', 'body').bind('scroll touchmove mousewheel', function(e)
    {
        //TODO: need to limit this to when the mouse is over the image in question

        //TODO: behavior not the same in Safari and FF, but seems to work in Edge and Chrome

        if (typeof e.originalEvent != 'undefined' && e.originalEvent != null
            && e.originalEvent.wheelDelta != 'undefined' && e.originalEvent.wheelDelta != null)
        {
            e.preventDefault();
            e.stopPropagation();
            console.log(e);
            if (e.originalEvent.wheelDelta > 0)
            {
                // zooming
                var newW = 1.1 * parseFloat(img.width());
                var newH = 1.1 * parseFloat(img.height());
                if (newW < el.naturalWidth && newH < el.naturalHeight)
                {
                    // Go ahead and zoom the image
                    //console.log('zooming the image');
                    img.css(
                    {
                        "width": newW + 'px',
                        "height": newH + 'px',
                        "max-width": newW + 'px',
                        "max-height": newH + 'px'
                    });
                }
                else
                {
                    // Make image as big as it gets
                    //console.log('making it as big as it gets');
                    img.css(
                    {
                        "width": el.naturalWidth + 'px',
                        "height": el.naturalHeight + 'px',
                        "max-width": el.naturalWidth + 'px',
                        "max-height": el.naturalHeight + 'px'
                    });
                }
            }
            else if (e.originalEvent.wheelDelta < 0)
            {
                // shrinking
                var newW = 0.9 * parseFloat(img.width());
                var newH = 0.9 * parseFloat(img.height());

                //TODO: I had added these data-attributes to the image onload.
                // They represent the original width and height of the image on the screen.
                // If your image is normally 100% width, you may need to change these values on resize.
                var origW = parseFloat(img.attr('data-startwidth'));
                var origH = parseFloat(img.attr('data-startheight'));

                if (newW > origW && newH > origH)
                {
                    // Go ahead and shrink the image
                    //console.log('shrinking the image');
                    img.css(
                    {
                        "width": newW + 'px',
                        "height": newH + 'px',
                        "max-width": newW + 'px',
                        "max-height": newH + 'px'
                    });
                }
                else
                {
                    // Make image as small as it gets
                    //console.log('making it as small as it gets');
                    // This restores the image to its original size. You may want
                    //to do this differently, like by removing the css instead of defining it.
                    img.css(
                    {
                        "width": origW + 'px',
                        "height": origH + 'px',
                        "max-width": origW + 'px',
                        "max-height": origH + 'px'
                    });
                }
            }
        }
    });
}

My answer is inspired by Jeffrey's answer. Where that answer gives a more abstract solution, I try to provide more concrete steps on how to potentially implement it. This is simply a guide, one that can be implemented more elegantly. For a more detailed example check out this tutorial by MDN web docs.

HTML:

<div id="zoom_here">....</div>

JS

<script>
var dist1=0;
function start(ev) {
           if (ev.targetTouches.length == 2) {//check if two fingers touched screen
               dist1 = Math.hypot( //get rough estimate of distance between two fingers
                ev.touches[0].pageX - ev.touches[1].pageX,
                ev.touches[0].pageY - ev.touches[1].pageY);                  
           }
    
    }
    function move(ev) {
           if (ev.targetTouches.length == 2 && ev.changedTouches.length == 2) {
                 // Check if the two target touches are the same ones that started
               var dist2 = Math.hypot(//get rough estimate of new distance between fingers
                ev.touches[0].pageX - ev.touches[1].pageX,
                ev.touches[0].pageY - ev.touches[1].pageY);
                //alert(dist);
                if(dist1>dist2) {//if fingers are closer now than when they first touched screen, they are pinching
                  alert('zoom out');
                }
                if(dist1<dist2) {//if fingers are further apart than when they first touched the screen, they are making the zoomin gesture
                   alert('zoom in');
                }
           }
           
    }
        document.getElementById ('zoom_here').addEventListener ('touchstart', start, false);
        document.getElementById('zoom_here').addEventListener('touchmove', move, false);
</script>

detect two fingers pinch zoom on any element, easy and w/o hassle with 3rd party libs like Hammer.js (beware, hammer has issues with scrolling!)

function onScale(el, callback) {
    let hypo = undefined;

    el.addEventListener('touchmove', function(event) {
        if (event.targetTouches.length === 2) {
            let hypo1 = Math.hypot((event.targetTouches[0].pageX - event.targetTouches[1].pageX),
                (event.targetTouches[0].pageY - event.targetTouches[1].pageY));
            if (hypo === undefined) {
                hypo = hypo1;
            }
            callback(hypo1/hypo);
        }
    }, false);


    el.addEventListener('touchend', function(event) {
        hypo = undefined;
    }, false);
}

Hammer.js all the way! It handles "transforms" (pinches). http://eightmedia.github.com/hammer.js/

But if you wish to implement it youself, i think that Jeffrey's answer is pretty solid.


Unfortunately, detecting pinch gestures across browsers is a not as simple as one would hope, but HammerJS makes it a lot easier!

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:

_x000D_
_x000D_
<!DOCTYPE html>_x000D_
<html>_x000D_
<head>_x000D_
  <meta charset="utf-8">_x000D_
  <meta name="viewport"_x000D_
        content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">_x000D_
  <title>Pinch Zoom</title>_x000D_
</head>_x000D_
_x000D_
<body>_x000D_
_x000D_
  <div>_x000D_
_x000D_
    <div style="height:150px;background-color:#eeeeee">_x000D_
      Ignore this area. Space is needed to test on the iPhone simulator as pinch simulation on the_x000D_
      iPhone simulator requires the target to be near the middle of the screen and we only respect_x000D_
      touch events in the image area. This space is not needed in production._x000D_
    </div>_x000D_
_x000D_
    <style>_x000D_
_x000D_
      .pinch-zoom-container {_x000D_
        overflow: hidden;_x000D_
        height: 300px;_x000D_
      }_x000D_
_x000D_
      .pinch-zoom-image {_x000D_
        width: 100%;_x000D_
      }_x000D_
_x000D_
    </style>_x000D_
_x000D_
    <script src="https://hammerjs.github.io/dist/hammer.js"></script>_x000D_
_x000D_
    <script>_x000D_
_x000D_
      var MIN_SCALE = 1; // 1=scaling when first loaded_x000D_
      var MAX_SCALE = 64;_x000D_
_x000D_
      // HammerJS fires "pinch" and "pan" events that are cumulative in nature and not_x000D_
      // deltas. Therefore, we need to store the "last" values of scale, x and y so that we can_x000D_
      // adjust the UI accordingly. It isn't until the "pinchend" and "panend" events are received_x000D_
      // that we can set the "last" values._x000D_
_x000D_
      // Our "raw" coordinates are not scaled. This allows us to only have to modify our stored_x000D_
      // coordinates when the UI is updated. It also simplifies our calculations as these_x000D_
      // coordinates are without respect to the current scale._x000D_
_x000D_
      var imgWidth = null;_x000D_
      var imgHeight = null;_x000D_
      var viewportWidth = null;_x000D_
      var viewportHeight = null;_x000D_
      var scale = null;_x000D_
      var lastScale = null;_x000D_
      var container = null;_x000D_
      var img = null;_x000D_
      var x = 0;_x000D_
      var lastX = 0;_x000D_
      var y = 0;_x000D_
      var lastY = 0;_x000D_
      var pinchCenter = null;_x000D_
_x000D_
      // We need to disable the following event handlers so that the browser doesn't try to_x000D_
      // automatically handle our image drag gestures._x000D_
      var disableImgEventHandlers = function () {_x000D_
        var events = ['onclick', 'onmousedown', 'onmousemove', 'onmouseout', 'onmouseover',_x000D_
                      'onmouseup', 'ondblclick', 'onfocus', 'onblur'];_x000D_
_x000D_
        events.forEach(function (event) {_x000D_
          img[event] = function () {_x000D_
            return false;_x000D_
          };_x000D_
        });_x000D_
      };_x000D_
_x000D_
      // Traverse the DOM to calculate the absolute position of an element_x000D_
      var absolutePosition = function (el) {_x000D_
        var x = 0,_x000D_
          y = 0;_x000D_
_x000D_
        while (el !== null) {_x000D_
          x += el.offsetLeft;_x000D_
          y += el.offsetTop;_x000D_
          el = el.offsetParent;_x000D_
        }_x000D_
_x000D_
        return { x: x, y: y };_x000D_
      };_x000D_
_x000D_
      var restrictScale = function (scale) {_x000D_
        if (scale < MIN_SCALE) {_x000D_
          scale = MIN_SCALE;_x000D_
        } else if (scale > MAX_SCALE) {_x000D_
          scale = MAX_SCALE;_x000D_
        }_x000D_
        return scale;_x000D_
      };_x000D_
_x000D_
      var restrictRawPos = function (pos, viewportDim, imgDim) {_x000D_
        if (pos < viewportDim/scale - imgDim) { // too far left/up?_x000D_
          pos = viewportDim/scale - imgDim;_x000D_
        } else if (pos > 0) { // too far right/down?_x000D_
          pos = 0;_x000D_
        }_x000D_
        return pos;_x000D_
      };_x000D_
_x000D_
      var updateLastPos = function (deltaX, deltaY) {_x000D_
        lastX = x;_x000D_
        lastY = y;_x000D_
      };_x000D_
_x000D_
      var translate = function (deltaX, deltaY) {_x000D_
        // We restrict to the min of the viewport width/height or current width/height as the_x000D_
        // current width/height may be smaller than the viewport width/height_x000D_
_x000D_
        var newX = restrictRawPos(lastX + deltaX/scale,_x000D_
                                  Math.min(viewportWidth, curWidth), imgWidth);_x000D_
        x = newX;_x000D_
        img.style.marginLeft = Math.ceil(newX*scale) + 'px';_x000D_
_x000D_
        var newY = restrictRawPos(lastY + deltaY/scale,_x000D_
                                  Math.min(viewportHeight, curHeight), imgHeight);_x000D_
        y = newY;_x000D_
        img.style.marginTop = Math.ceil(newY*scale) + 'px';_x000D_
      };_x000D_
_x000D_
      var zoom = function (scaleBy) {_x000D_
        scale = restrictScale(lastScale*scaleBy);_x000D_
_x000D_
        curWidth = imgWidth*scale;_x000D_
        curHeight = imgHeight*scale;_x000D_
_x000D_
        img.style.width = Math.ceil(curWidth) + 'px';_x000D_
        img.style.height = Math.ceil(curHeight) + 'px';_x000D_
_x000D_
        // Adjust margins to make sure that we aren't out of bounds_x000D_
        translate(0, 0);_x000D_
      };_x000D_
_x000D_
      var rawCenter = function (e) {_x000D_
        var pos = absolutePosition(container);_x000D_
_x000D_
        // We need to account for the scroll position_x000D_
        var scrollLeft = window.pageXOffset ? window.pageXOffset : document.body.scrollLeft;_x000D_
        var scrollTop = window.pageYOffset ? window.pageYOffset : document.body.scrollTop;_x000D_
_x000D_
        var zoomX = -x + (e.center.x - pos.x + scrollLeft)/scale;_x000D_
        var zoomY = -y + (e.center.y - pos.y + scrollTop)/scale;_x000D_
_x000D_
        return { x: zoomX, y: zoomY };_x000D_
      };_x000D_
_x000D_
      var updateLastScale = function () {_x000D_
        lastScale = scale;_x000D_
      };_x000D_
_x000D_
      var zoomAround = function (scaleBy, rawZoomX, rawZoomY, doNotUpdateLast) {_x000D_
        // Zoom_x000D_
        zoom(scaleBy);_x000D_
_x000D_
        // New raw center of viewport_x000D_
        var rawCenterX = -x + Math.min(viewportWidth, curWidth)/2/scale;_x000D_
        var rawCenterY = -y + Math.min(viewportHeight, curHeight)/2/scale;_x000D_
_x000D_
        // Delta_x000D_
        var deltaX = (rawCenterX - rawZoomX)*scale;_x000D_
        var deltaY = (rawCenterY - rawZoomY)*scale;_x000D_
_x000D_
        // Translate back to zoom center_x000D_
        translate(deltaX, deltaY);_x000D_
_x000D_
        if (!doNotUpdateLast) {_x000D_
          updateLastScale();_x000D_
          updateLastPos();_x000D_
        }_x000D_
      };_x000D_
_x000D_
      var zoomCenter = function (scaleBy) {_x000D_
        // Center of viewport_x000D_
        var zoomX = -x + Math.min(viewportWidth, curWidth)/2/scale;_x000D_
        var zoomY = -y + Math.min(viewportHeight, curHeight)/2/scale;_x000D_
_x000D_
        zoomAround(scaleBy, zoomX, zoomY);_x000D_
      };_x000D_
_x000D_
      var zoomIn = function () {_x000D_
        zoomCenter(2);_x000D_
      };_x000D_
_x000D_
      var zoomOut = function () {_x000D_
        zoomCenter(1/2);_x000D_
      };_x000D_
_x000D_
      var onLoad = function () {_x000D_
_x000D_
        img = document.getElementById('pinch-zoom-image-id');_x000D_
        container = img.parentElement;_x000D_
_x000D_
        disableImgEventHandlers();_x000D_
_x000D_
        imgWidth = img.width;_x000D_
        imgHeight = img.height;_x000D_
        viewportWidth = img.offsetWidth;_x000D_
        scale = viewportWidth/imgWidth;_x000D_
        lastScale = scale;_x000D_
        viewportHeight = img.parentElement.offsetHeight;_x000D_
        curWidth = imgWidth*scale;_x000D_
        curHeight = imgHeight*scale;_x000D_
_x000D_
        var hammer = new Hammer(container, {_x000D_
          domEvents: true_x000D_
        });_x000D_
_x000D_
        hammer.get('pinch').set({_x000D_
          enable: true_x000D_
        });_x000D_
_x000D_
        hammer.on('pan', function (e) {_x000D_
          translate(e.deltaX, e.deltaY);_x000D_
        });_x000D_
_x000D_
        hammer.on('panend', function (e) {_x000D_
          updateLastPos();_x000D_
        });_x000D_
_x000D_
        hammer.on('pinch', function (e) {_x000D_
_x000D_
          // We only calculate the pinch center on the first pinch event as we want the center to_x000D_
          // stay consistent during the entire pinch_x000D_
          if (pinchCenter === null) {_x000D_
            pinchCenter = rawCenter(e);_x000D_
            var offsetX = pinchCenter.x*scale - (-x*scale + Math.min(viewportWidth, curWidth)/2);_x000D_
            var offsetY = pinchCenter.y*scale - (-y*scale + Math.min(viewportHeight, curHeight)/2);_x000D_
            pinchCenterOffset = { x: offsetX, y: offsetY };_x000D_
          }_x000D_
_x000D_
          // When the user pinch zooms, she/he expects the pinch center to remain in the same_x000D_
          // relative location of the screen. To achieve this, the raw zoom center is calculated by_x000D_
          // first storing the pinch center and the scaled offset to the current center of the_x000D_
          // image. The new scale is then used to calculate the zoom center. This has the effect of_x000D_
          // actually translating the zoom center on each pinch zoom event._x000D_
          var newScale = restrictScale(scale*e.scale);_x000D_
          var zoomX = pinchCenter.x*newScale - pinchCenterOffset.x;_x000D_
          var zoomY = pinchCenter.y*newScale - pinchCenterOffset.y;_x000D_
          var zoomCenter = { x: zoomX/newScale, y: zoomY/newScale };_x000D_
_x000D_
          zoomAround(e.scale, zoomCenter.x, zoomCenter.y, true);_x000D_
        });_x000D_
_x000D_
        hammer.on('pinchend', function (e) {_x000D_
          updateLastScale();_x000D_
          updateLastPos();_x000D_
          pinchCenter = null;_x000D_
        });_x000D_
_x000D_
        hammer.on('doubletap', function (e) {_x000D_
          var c = rawCenter(e);_x000D_
          zoomAround(2, c.x, c.y);_x000D_
        });_x000D_
_x000D_
      };_x000D_
_x000D_
    </script>_x000D_
_x000D_
    <button onclick="zoomIn()">Zoom In</button>_x000D_
    <button onclick="zoomOut()">Zoom Out</button>_x000D_
_x000D_
    <div class="pinch-zoom-container">_x000D_
      <img id="pinch-zoom-image-id" class="pinch-zoom-image" onload="onLoad()"_x000D_
           src="https://hammerjs.github.io/assets/img/pano-1.jpg">_x000D_
    </div>_x000D_
_x000D_
_x000D_
  </div>_x000D_
_x000D_
</body>_x000D_
</html>
_x000D_
_x000D_
_x000D_


Think about what a pinch event is: two fingers on an element, moving toward or away from each other. Gesture events are, to my knowledge, a fairly new standard, so probably the safest way to go about this is to use touch events like so:

(ontouchstart event)

if (e.touches.length === 2) {
    scaling = true;
    pinchStart(e);
}

(ontouchmove event)

if (scaling) {
    pinchMove(e);
}

(ontouchend event)

if (scaling) {
    pinchEnd(e);
    scaling = false;
}

To get the distance between the two fingers, use the hypot function:

var dist = Math.hypot(
    e.touches[0].pageX - e.touches[1].pageX,
    e.touches[0].pageY - e.touches[1].pageY);

Examples related to javascript

need to add a class to an element How to make a variable accessible outside a function? Hide Signs that Meteor.js was Used How to create a showdown.js markdown extension Please help me convert this script to a simple image slider Highlight Anchor Links when user manually scrolls? Summing radio input values How to execute an action before close metro app WinJS javascript, for loop defines a dynamic variable name Getting all files in directory with ajax

Examples related to jquery

How to make a variable accessible outside a function? Jquery assiging class to th in a table Please help me convert this script to a simple image slider Highlight Anchor Links when user manually scrolls? Getting all files in directory with ajax Bootstrap 4 multiselect dropdown Cross-Origin Read Blocking (CORB) bootstrap 4 file input doesn't show the file name Jquery AJAX: No 'Access-Control-Allow-Origin' header is present on the requested resource how to remove json object key and value.?

Examples related to ios

Adding a UISegmentedControl to UITableView Crop image to specified size and picture location Undefined Symbols error when integrating Apptentive iOS SDK via Cocoapods Keep placeholder text in UITextField on input in IOS Accessing AppDelegate from framework? Autoresize View When SubViews are Added Warp \ bend effect on a UIView? Speech input for visually impaired users without the need to tap the screen make UITableViewCell selectable only while editing Xcode 12, building for iOS Simulator, but linking in object file built for iOS, for architecture arm64

Examples related to mobile-safari

disable viewport zooming iOS 10+ safari? iOS 8 removed "minimal-ui" viewport property, are there other "soft fullscreen" solutions? HTML5 Video tag not working in Safari , iPhone and iPad CSS background-size: cover replacement for Mobile Safari Setting format and value in input type="date" Mobile overflow:scroll and overflow-scrolling: touch // prevent viewport "bounce" How to check if an app is installed from a web-page on an iPhone? Is Safari on iOS 6 caching $.ajax results? How to launch Safari and open URL from iOS app Simplest way to detect a pinch