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:
Those events will trigger every time a finger touches a touchscreen, but for us, only two fingers are relevant, so let's consider this:
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:
- Your two fingers start 50 pixels apart
- You keep moving, until they are 150 pixels apart
- That means you have a ratio of 3x the original distance, so the image will zoom 3x
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.
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 :)