How to Map Pinch to Zoom Gestures to SVG ViewBox
Mapping pinch-to-zoom gestures to an SVG viewBox
involves capturing multi-touch screen coordinates, calculating the
relative scale and focal center between two fingers, and mathematically
updating the viewBox origin and dimensions in real time. By
converting client touch coordinates into the SVG coordinate space, you
can dynamically adjust the min-x, min-y,
width, and height properties to ensure smooth,
responsive zooming centered precisely where the user pinches.
The SVG ViewBox Structure
An SVG viewBox attribute consists of four values:
min-x, min-y, width, and
height. * Decreasing width and
height zooms in. * Increasing width and
height zooms out. * Adjusting min-x and
min-y pans the viewport, keeping the zoom centered under
the user’s fingers.
Step 1: Capture Touch Events
Pinch gestures require monitoring two touch points using standard DOM
touch events: touchstart, touchmove, and
touchend.
let initialDistance = 0;
let initialViewBox = { x: 0, y: 0, w: 1000, h: 1000 };
let focalPoint = { x: 0, y: 0 };
svgElement.addEventListener('touchstart', (e) => {
if (e.touches.length === 2) {
e.preventDefault();
initialDistance = getDistance(e.touches[0], e.touches[1]);
initialViewBox = getCurrentViewBox();
focalPoint = getTouchCenterInSvg(e.touches[0], e.touches[1]);
}
});Step 2: Calculate Pinch Distance and Focal Point
Use the Pythagorean theorem to calculate the Euclidean distance between the two touches. The center point between the two fingers serves as the focal point of the zoom.
function getDistance(t1, t2) {
const dx = t1.clientX - t2.clientX;
const dy = t1.clientY - t2.clientY;
return Math.hypot(dx, dy);
}
function getTouchCenter(t1, t2) {
return {
x: (t1.clientX + t2.clientX) / 2,
y: (t1.clientY + t2.clientY) / 2
};
}Step 3: Convert Screen Coordinates to SVG Space
To ensure the zoom focuses accurately on the user’s touch location
regardless of CSS scaling, transform the screen coordinates into SVG
coordinate space using the element’s Current Transformation Matrix
(getScreenCTM).
function screenToSVG(svg, clientX, clientY) {
const pt = svg.createSVGPoint();
pt.x = clientX;
pt.y = clientY;
return pt.matrixTransform(svg.getScreenCTM().inverse());
}Step 4: Calculate and Apply the New ViewBox
During touchmove, determine the scale factor by dividing
the current distance between touches by the initial distance. Update the
viewBox width and height by this factor, and adjust the
origin (x and y) relative to the focal point
so the image expands or contracts around the pinch center.
svgElement.addEventListener('touchmove', (e) => {
if (e.touches.length === 2) {
e.preventDefault();
const currentDistance = getDistance(e.touches[0], e.touches[1]);
const scale = initialDistance / currentDistance;
// Calculate new dimensions
const newWidth = initialViewBox.w * scale;
const newHeight = initialViewBox.h * scale;
// Adjust origin around the SVG-space focal point
const newX = focalPoint.x - (focalPoint.x - initialViewBox.x) * scale;
const newY = focalPoint.y - (focalPoint.y - initialViewBox.y) * scale;
// Apply the updated viewBox
svgElement.setAttribute('viewBox', `${newX} ${newY} ${newWidth} ${newHeight}`);
}
});Performance and Edge Cases
- Passive Listeners: Set
{ passive: false }when adding touch listeners to allowe.preventDefault(), which prevents the default browser page zoom. - Constraints: Apply minimum and maximum width/height
limits to the calculated
viewBoxdimensions to prevent inverted scaling or excessive zooming. - Rendering Performance: Use
requestAnimationFrameto decouple touch event triggers from DOM updates, ensuring smooth rendering at the display’s native refresh rate.