NEW Pinch and Zoom (like Instagram)

A few weeks ago I published a blog post Pinch and Zoom like Instagram.

Turns out I didn't properly test on iOS and the effect was flaky. Here is simpler and more efficient solution using only native JavaScript, no library involved like Hammer.js in my first blog post.

Try this new DEMO on a mobile device.

Goal

The goal is to allow two fingers to pinch an image on a mobile device and allow the image to zoom and move, while the page body stays still.

When the two fingers are released, the image should go back to its original format.

This pattern follows how Instagram implements the pinch and zoom behaviour.

Implementation

Instead of relying on Hammer.js this time (which has not been updated in over two years, ahem) to deal with mobile gesture events, I decided to write my own in vanilla JavaScript.

We will need to listen to 3 events:

const imageElement = document.getElementById("P1_IMAGE");

imageElement.addEventListener('touchstart', (event) => {
  console.log('touchstart', event);
});

imageElement.addEventListener('touchmove', (event) => {
  console.log('touchmove', event);
});

imageElement.addEventListener('touchend', (event) => {
  console.log('touchend', event);
});
Listening to touch events on P1_IMAGE

Those events will trigger every time a finger touches a touchscreen, but for us, only two fingers are relevant, so let's consider this:

imageElement.addEventListener('touchmove', (event) => {
  if (event.touches.length === 2) {
    event.preventDefault(); // Prevent page scroll
  }
});
When two fingers are detected, prevent the default behaviour (page scrolling)

The following function will be useful to calculate the distance between the two fingers, which we will use later to calculate the how much you are zooming over time. Example:

  1. Your two fingers start 50 pixels apart
  2. You keep moving, until they are 150 pixels apart
  3. That means you have a ratio of 3x the original distance, so the image will zoom 3x
Credit: https://stackoverflow.com/a/20916980/2524979
  const distance = (event) => {
      return Math.hypot(event.touches[0].pageX - event.touches[1].pageX, event.touches[0].pageY - event.touches[1].pageY);
  };
I had no idea I would be using high school mathematics again in my life...

Now we have all we need for the pinch and zoom. Let's start with getting the initial coordinates of the fingers. The original X and Y coordinates will be set to the MIDDLE point between your two fingers. Then we calculate the distance between the two fingers when they start touching the screen.

  imageElement.addEventListener('touchstart', (event) => {
    if (event.touches.length === 2) {
      event.preventDefault(); // Prevent page scroll

      // Calculate where the fingers have started on the X and Y axis
      start.x = (event.touches[0].pageX + event.touches[1].pageX) / 2;
      start.y = (event.touches[0].pageY + event.touches[1].pageY) / 2;
      start.distance = distance(event);
    }
  });
Middle point for X is (X1 + X2) / 2. Same for Y

Then apply CSS transformation on the image as the two fingers move (pinching):

  imageElement.addEventListener('touchmove', (event) => {
    if (event.touches.length === 2) {
      event.preventDefault(); // Prevent page scroll

      // Safari provides event.scale as two fingers move on the screen
      // For other browsers just calculate the scale manually
      let scale;
      if (event.scale) {
        scale = event.scale;
      } else {
        const deltaDistance = distance(event);
        scale = deltaDistance / start.distance;
      }
      imageElementScale = Math.min(Math.max(1, scale), 4);

      // Calculate how much the fingers have moved on the X and Y axis
      const deltaX = (((event.touches[0].pageX + event.touches[1].pageX) / 2) - start.x) * 2; // x2 for accelarated movement
      const deltaY = (((event.touches[0].pageY + event.touches[1].pageY) / 2) - start.y) * 2; // x2 for accelarated movement

      // Transform the image to make it grow and move with fingers
      const transform = `translate3d(${deltaX}px, ${deltaY}px, 0) scale(${imageElementScale})`;
      imageElement.style.transform = transform;
      imageElement.style.WebkitTransform = transform;
      imageElement.style.zIndex = "9999";
    }
  });

And finally reset the image to its original format when the two fingers are released:

  imageElement.addEventListener('touchend', (event) => {
    // Reset image to it's original format
    imageElement.style.transform = "";
    imageElement.style.WebkitTransform = "";
    imageElement.style.zIndex = "";
  });	

Final code

Here is the final code, 59 lines of JavaScript wrapped in a function pinchZoom for reusability.

const pinchZoom = (imageElement) => {
  let imageElementScale = 1;

  let start = {};

  // Calculate distance between two fingers
  const distance = (event) => {
    return Math.hypot(event.touches[0].pageX - event.touches[1].pageX, event.touches[0].pageY - event.touches[1].pageY);
  };

  imageElement.addEventListener('touchstart', (event) => {
    console.log('touchstart', event);
    if (event.touches.length === 2) {
      event.preventDefault(); // Prevent page scroll

      // Calculate where the fingers have started on the X and Y axis
      start.x = (event.touches[0].pageX + event.touches[1].pageX) / 2;
      start.y = (event.touches[0].pageY + event.touches[1].pageY) / 2;
      start.distance = distance(event);
    }
  });

  imageElement.addEventListener('touchmove', (event) => {
    console.log('touchmove', event);
    if (event.touches.length === 2) {
      event.preventDefault(); // Prevent page scroll
      let scale;

      // Safari provides event.scale as two fingers move on the screen
      // For other browsers just calculate the scale manually
      if (event.scale) {
        scale = event.scale;
      } else {
        const deltaDistance = distance(event);
        scale = deltaDistance / start.distance;
      }

      imageElementScale = Math.min(Math.max(1, scale), 4);

      // Calculate how much the fingers have moved on the X and Y axis
      const deltaX = (((event.touches[0].pageX + event.touches[1].pageX) / 2) - start.x) * 2; // x2 for accelarated movement
      const deltaY = (((event.touches[0].pageY + event.touches[1].pageY) / 2) - start.y) * 2; // x2 for accelarated movement

      // Transform the image to make it grow and move with fingers
      const transform = `translate3d(${deltaX}px, ${deltaY}px, 0) scale(${imageElementScale})`;
      imageElement.style.transform = transform;
      imageElement.style.WebkitTransform = transform;
      imageElement.style.zIndex = "9999";
    }
  });

  imageElement.addEventListener('touchend', (event) => {
    console.log('touchend', event);
    // Reset image to it's original format
    imageElement.style.transform = "";
    imageElement.style.WebkitTransform = "";
    imageElement.style.zIndex = "";
  });
}

And a drop of CSS to allow to the image to overflow over the Card Region (you might not need this):

.js-resize-sensor, .t-CardsRegion, .a-CardView-media, .a-TMV-w-scroll {
   overflow: visible!important;
}

To arrive to this solution, I had to mix countless Stackoverflow snippets. None of them worked. Hammer.js failed on iOS. So I ended up writing the thing myself. It's one of those situations where you can't copy other people's work, you have to understand it yourself.

...and I got to apply high school mathematics again which felt quite good :)