UNPKG

svelte-image-viewer

Version:

A couple of simple components for displaying content with pan and zoom capabilities.

128 lines (127 loc) 5.67 kB
export default function panAndZoom(element, { offsetX, offsetY, scale, minScale = 0.25, maxScale = 2.0, scaleSmoothing = 500, }) { const pointers = new Map(); let initialDistance = 0; let initialScale = scale.get(); let currentOffsetX = offsetX.get(); let currentOffsetY = offsetY.get(); let initialMidpointX = 0; let initialMidpointY = 0; let centreX = 0; let centreY = 0; function handlePointerDown(event) { event.preventDefault(); pointers.set(event.pointerId, event); element.setPointerCapture(event.pointerId); if (pointers.size === 1) { currentOffsetX = offsetX.get(); currentOffsetY = offsetY.get(); initialMidpointX = event.clientX; initialMidpointY = event.clientY; } else if (pointers.size === 2) { const [p1, p2] = Array.from(pointers.values()); initialDistance = distance(p1.clientX, p1.clientY, p2.clientX, p2.clientY); initialScale = scale.get(); currentOffsetX = offsetX.get(); currentOffsetY = offsetY.get(); [initialMidpointX, initialMidpointY] = midpoint(p1.clientX, p1.clientY, p2.clientX, p2.clientY); const rect = element.getBoundingClientRect(); centreX = (initialMidpointX - rect.left - rect.width * 0.5 - currentOffsetX) / initialScale; centreY = (initialMidpointY - rect.top - rect.height * 0.5 - currentOffsetY) / initialScale; } } function handlePointerMove(event) { if (!pointers.has(event.pointerId)) { return; } event.preventDefault(); pointers.set(event.pointerId, event); if (pointers.size === 1) { const pointer = pointers.get(event.pointerId); if (pointer === undefined) { return; } const dx = pointer.clientX - initialMidpointX; const dy = pointer.clientY - initialMidpointY; const newX = currentOffsetX + dx; const newY = currentOffsetY + dy; offsetX.set(newX); offsetY.set(newY); } else if (pointers.size === 2) { const [p1, p2] = Array.from(pointers.values()); const currentDistance = distance(p1.clientX, p1.clientY, p2.clientX, p2.clientY); const scaleChange = currentDistance / initialDistance; const newScale = clamp(initialScale * scaleChange, minScale, maxScale); const [currentMidpointX, currentMidpointY] = midpoint(p1.clientX, p1.clientY, p2.clientX, p2.clientY); const rect = element.getBoundingClientRect(); const newOffsetX = currentMidpointX - rect.left - rect.width * 0.5 - newScale * centreX; const newOffsetY = currentMidpointY - rect.top - rect.height * 0.5 - newScale * centreY; offsetX.set(newOffsetX); offsetY.set(newOffsetY); scale.set(newScale); } } function handlePointerUp(event) { event.preventDefault(); pointers.delete(event.pointerId); element.releasePointerCapture(event.pointerId); if (pointers.size === 1) { const pointer = Array.from(pointers.values()).at(0); if (pointer === undefined) { return; } currentOffsetX = offsetX.get(); currentOffsetY = offsetY.get(); initialMidpointX = pointer.clientX; initialMidpointY = pointer.clientY; } } function handleWheel(event) { event.preventDefault(); const delta = -event.deltaY; const scaleChange = 1 + delta / scaleSmoothing; const currentScale = scale.get(); const newScale = clamp(currentScale * scaleChange, minScale, maxScale); const adjustedScale = newScale / currentScale; const rect = element.getBoundingClientRect(); const newOffsetX = event.clientX - rect.left - rect.width * 0.5; const newOffsetY = event.clientY - rect.top - rect.height * 0.5; const newX = newOffsetX - adjustedScale * (newOffsetX - offsetX.get()); const newY = newOffsetY - adjustedScale * (newOffsetY - offsetY.get()); offsetX.set(newX); offsetY.set(newY); scale.set(newScale); } element.addEventListener("pointerdown", handlePointerDown); element.addEventListener("pointermove", handlePointerMove); element.addEventListener("pointerup", handlePointerUp); element.addEventListener("pointercancel", handlePointerUp); element.addEventListener("pointerleave", handlePointerUp); element.addEventListener("pointerout", handlePointerUp); element.addEventListener("wheel", handleWheel, { passive: false }); return { destroy() { element.removeEventListener("pointerdown", handlePointerDown); element.removeEventListener("pointermove", handlePointerMove); element.removeEventListener("pointerup", handlePointerUp); element.removeEventListener("pointercancel", handlePointerUp); element.removeEventListener("pointerleave", handlePointerUp); element.removeEventListener("pointerout", handlePointerUp); element.removeEventListener("wheel", handleWheel); }, }; } function clamp(value, min, max) { return value < min ? min : value > max ? max : value; } function distance(x1, y1, x2, y2) { return Math.hypot(x2 - x1, y2 - y1); } function midpoint(x1, y1, x2, y2) { return [(x1 + x2) / 2, (y1 + y2) / 2]; }