Skip to content

Commit 7a07cb4

Browse files
committed
Improvement - Add support for pinch zoom in usePanZoom composable
1 parent 9885c68 commit 7a07cb4

File tree

2 files changed

+57
-13
lines changed

2 files changed

+57
-13
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,4 @@
107107
"vitest": "^3.1.1",
108108
"vue": "^3.5.13"
109109
}
110-
}
110+
}

src/usePanZoom.js

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,21 @@ export default function usePanZoom(svgRef, initialViewBox = { x: 0, y: 0, width:
66
const scale = ref(1);
77
const isPanning = ref(false);
88
const startPoint = ref({ x: 0, y: 0 });
9+
const pinchStartDist = ref(0);
10+
const pinchStartViewBox = ref(null);
11+
const isPinching = ref(false);
912

1013
let velocity = { x: 0, y: 0 };
1114
let animationFrame = null;
1215
let zoomAnimationFrame = null;
1316

17+
function getTouchDistance(touches) {
18+
if (touches.length < 2) return 0;
19+
const dx = touches[0].clientX - touches[1].clientX;
20+
const dy = touches[0].clientY - touches[1].clientY;
21+
return Math.sqrt(dx * dx + dy * dy);
22+
}
23+
1424
function toSvgPoint(event) {
1525
const svg = svgRef.value;
1626
if (!svg) return { x: 0, y: 0 };
@@ -119,6 +129,44 @@ export default function usePanZoom(svgRef, initialViewBox = { x: 0, y: 0, width:
119129
scale.value = newScale;
120130
};
121131

132+
function handleTouchStart(event) {
133+
if (event.touches.length === 2) {
134+
isPinching.value = true;
135+
pinchStartDist.value = getTouchDistance(event.touches);
136+
pinchStartViewBox.value = { ...viewBox.value };
137+
} else {
138+
event.preventDefault();
139+
startPan(event);
140+
}
141+
}
142+
143+
function handleTouchMove(event) {
144+
if (isPinching.value && event.touches.length === 2) {
145+
event.preventDefault();
146+
const dist = getTouchDistance(event.touches);
147+
if (pinchStartDist.value) {
148+
const zoomFactor = dist / pinchStartDist.value;
149+
const svg = svgRef.value;
150+
const rect = svg.getBoundingClientRect();
151+
const midX = (event.touches[0].clientX + event.touches[1].clientX) / 2 - rect.left;
152+
const midY = (event.touches[0].clientY + event.touches[1].clientY) / 2 - rect.top;
153+
const midPoint = toSvgPoint({ clientX: midX + rect.left, clientY: midY + rect.top });
154+
viewBox.value = { ...pinchStartViewBox.value };
155+
applyZoom(zoomFactor, midPoint);
156+
}
157+
} else {
158+
event.preventDefault();
159+
doPan(event);
160+
}
161+
}
162+
163+
function handleTouchEnd(event) {
164+
if (event.touches.length < 2) {
165+
isPinching.value = false;
166+
}
167+
endPan();
168+
}
169+
122170
onMounted(addEventListeners);
123171
onUnmounted(removeEventListeners);
124172

@@ -132,15 +180,10 @@ export default function usePanZoom(svgRef, initialViewBox = { x: 0, y: 0, width:
132180
svg.addEventListener('mouseleave', endPan);
133181
svg.addEventListener('wheel', zoom, { passive: false });
134182
svg.addEventListener('dblclick', doubleClickZoom);
135-
svg.addEventListener('touchstart', (event) => {
136-
event.preventDefault();
137-
startPan(event);
138-
}, { passive: false });
139-
svg.addEventListener('touchmove', (event) => {
140-
event.preventDefault();
141-
doPan(event);
142-
}, { passive: false });
143-
svg.addEventListener('touchend', endPan);
183+
svg.addEventListener('touchstart', handleTouchStart, { passive: false });
184+
svg.addEventListener('touchmove', handleTouchMove, { passive: false });
185+
svg.addEventListener('touchend', handleTouchEnd);
186+
svg.addEventListener('touchcancel', handleTouchEnd);
144187
}
145188

146189
function removeEventListeners() {
@@ -152,9 +195,10 @@ export default function usePanZoom(svgRef, initialViewBox = { x: 0, y: 0, width:
152195
svg.removeEventListener('mouseleave', endPan);
153196
svg.removeEventListener('wheel', zoom);
154197
svg.removeEventListener('dblclick', doubleClickZoom);
155-
svg.removeEventListener('touchstart', startPan);
156-
svg.removeEventListener('touchmove', doPan);
157-
svg.removeEventListener('touchend', endPan);
198+
svg.removeEventListener('touchstart', handleTouchStart);
199+
svg.removeEventListener('touchmove', handleTouchMove);
200+
svg.removeEventListener('touchend', handleTouchEnd);
201+
svg.removeEventListener('touchcancel', handleTouchEnd);
158202
}
159203

160204
watchEffect(() => {

0 commit comments

Comments
 (0)