I have the following script to zoom and pan image. Mouse functionality works ok including wheel, but I want to add touch support for mobile. I can figure out how to calculate current scale when pinch starts and retain image position without any ind of weird jumps.
const img = document.getElementById('image');
const stage = document.querySelector('.stage');
const container = document.querySelector('.container');
let mouseX;
let mouseY;
let mouseTX;
let mouseTY;
let startX = 0;
let startY = 0;
let panning = false;
const ts = {
scale: 1,
translate: {
x: 0,
y: 0
}
};
stage.onwheel = function(event) {
event.preventDefault();
var func = stage.onwheel;
stage.onwheel = null;
let rec = stage.getBoundingClientRect();
let x = (event.clientX - rec.x) / ts.scale;
let y = (event.clientY - rec.y) / ts.scale;
let delta = (event.wheelDelta ? event.wheelDelta : -event.deltaY);
ts.scale = (delta > 0) ? (ts.scale + 0.2) : (ts.scale - 0.2);
if (ts.scale < 1) ts.scale = 1
let m = (delta > 0) ? 0.1 : -0.1;
ts.translate.x += (-x * m * 2) + (stage.offsetWidth * m);
ts.translate.y += (-y * m * 2) + (stage.offsetHeight * m);
setTransform();
stage.onwheel = func;
};
stage.onmousedown = function(event) {
event.preventDefault();
panning = true;
mouseX = event.clientX;
mouseY = event.clientY;
mouseTX = ts.translate.x;
mouseTY = ts.translate.y;
};
stage.onmouseup = function(event) {
panning = false;
};
document.onmouseup = function(event) {
panning = false;
};
stage.onmousemove = function(event) {
event.preventDefault();
let rec = stage.getBoundingClientRect();
const x = event.clientX;
const y = event.clientY;
if (!panning) {
return;
}
ts.translate.x = mouseTX + (x - mouseX);
ts.translate.y = mouseTY + (y - mouseY);
setTransform();
};
function setTransform() {
const steps = `translate(${ts.translate.x}px,${ts.translate.y}px) scale(${ts.scale}) translate3d(0,0,0)`;
stage.style.transform = steps;
}
function reset() {
ts.scale = 1;
ts.translate = {
x: 0,
y: 0
};
stage.style.transform = 'none';
}
setTransform();
if ("ontouchstart" in window) {
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);
};
stage.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);
}
});
stage.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;
}
var 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);
const deltaY = (((event.touches[0].pageY + event.touches[1].pageY) / 2) - start.y);
ts.translate.x = deltaX;
ts.translate.y = deltaY;
ts.scale = imageElementScale
setTransform();
}
});
stage.addEventListener('touchend', (event) => {
panning = false;
});
}
.container {
position: relative;
width: 600px;
border: 1px solid;
overflow: hidden;
}
.stage {
transform-origin: 50% 50%;
cursor: grab;
position: relative;
width: 100%;
}
#image {
width: 100%;
height: auto;
display: block;
}
<div class="container">
<div class="stage">
<img id="image" src="https://cdn.pixabay.com/photo/2018/01/14/23/12/nature-3082832__480.jpg" />
</div>
</div>