Skip to content

Commit f3068eb

Browse files
committed
Composables - Add usePanZoom
1 parent c87d10f commit f3068eb

File tree

1 file changed

+159
-0
lines changed

1 file changed

+159
-0
lines changed

src/usePanZoom.js

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { ref, onMounted, onUnmounted } from 'vue';
2+
3+
export default function usePanZoom(svgRef, initialViewBox = { x: 0, y: 0, width: 100, height: 100 }, speed = 1) {
4+
const viewBox = ref({ ...initialViewBox });
5+
const scale = ref(1);
6+
const isPanning = ref(false);
7+
const startPoint = ref({ x: 0, y: 0 });
8+
9+
let velocity = { x: 0, y: 0 };
10+
let animationFrame = null;
11+
let zoomAnimationFrame = null;
12+
13+
function toSvgPoint(event) {
14+
const svg = svgRef.value;
15+
if (!svg) return { x: 0, y: 0 };
16+
17+
const point = svg.createSVGPoint();
18+
point.x = event.clientX;
19+
point.y = event.clientY;
20+
21+
const matrix = svg.getScreenCTM()?.inverse();
22+
return matrix ? point.matrixTransform(matrix) : { x: 0, y: 0 };
23+
};
24+
25+
function startPan(event) {
26+
isPanning.value = true;
27+
const point = toSvgPoint(event.touches ? event.touches[0] : event);
28+
startPoint.value = { x: point.x, y: point.y };
29+
velocity = { x: 0, y: 0 };
30+
};
31+
32+
function doPan(event) {
33+
if (!isPanning.value) return;
34+
35+
const point = toSvgPoint(event.touches ? event.touches[0] : event);
36+
37+
let dx = point.x - startPoint.value.x;
38+
let dy = point.y - startPoint.value.y;
39+
40+
if (Math.abs(dx) < 0.3 && Math.abs(dy) < 0.3) return;
41+
42+
velocity.x = dx * 0.8 + velocity.x * 0.2;
43+
velocity.y = dy * 0.8 + velocity.y * 0.2;
44+
45+
startPoint.value = point;
46+
47+
if (!animationFrame) {
48+
animationFrame = requestAnimationFrame(applyPan);
49+
}
50+
};
51+
52+
function applyPan() {
53+
viewBox.value.x -= velocity.x;
54+
viewBox.value.y -= velocity.y;
55+
animationFrame = null;
56+
};
57+
58+
function endPan() {
59+
isPanning.value = false;
60+
};
61+
62+
function zoom(event) {
63+
event.preventDefault();
64+
const zoomFactor = event.deltaY > 0 ? 0.9 : 1.1;
65+
applyZoom(zoomFactor, toSvgPoint(event));
66+
};
67+
68+
function doubleClickZoom(event) {
69+
console.log(speed)
70+
event.preventDefault();
71+
const cursorPoint = toSvgPoint(event);
72+
const zoomFactor = 1.02 * (1 + (speed / 100)); // Always zoom in
73+
74+
animateZoom(zoomFactor, cursorPoint);
75+
};
76+
77+
function animateZoom(zoomFactor, cursorPoint) {
78+
if (zoomAnimationFrame) cancelAnimationFrame(zoomAnimationFrame);
79+
80+
let startScale = scale.value;
81+
let targetScale = startScale * zoomFactor;
82+
let progress = 0;
83+
84+
const animate = () => {
85+
progress += 0.02;
86+
87+
let currentScale = startScale + (targetScale - startScale) * easeInOutQuad(progress);
88+
89+
if (progress >= 1) {
90+
scale.value = targetScale;
91+
zoomAnimationFrame = null;
92+
return;
93+
}
94+
95+
applyZoom(currentScale / startScale, cursorPoint);
96+
zoomAnimationFrame = requestAnimationFrame(animate);
97+
};
98+
99+
animate();
100+
};
101+
102+
const easeInOutQuad = (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
103+
104+
function applyZoom(zoomFactor, cursorPoint) {
105+
const newScale = scale.value * zoomFactor;
106+
const scaleFactor = newScale / scale.value;
107+
108+
const newWidth = viewBox.value.width / scaleFactor;
109+
const newHeight = viewBox.value.height / scaleFactor;
110+
111+
const dx = (cursorPoint.x - viewBox.value.x) * (1 - 1 / scaleFactor);
112+
const dy = (cursorPoint.y - viewBox.value.y) * (1 - 1 / scaleFactor);
113+
114+
viewBox.value.x += dx;
115+
viewBox.value.y += dy;
116+
viewBox.value.width = newWidth;
117+
viewBox.value.height = newHeight;
118+
119+
scale.value = newScale;
120+
};
121+
122+
onMounted(() => {
123+
const svg = svgRef.value;
124+
if (!svg) return;
125+
126+
svg.addEventListener('mousedown', startPan);
127+
svg.addEventListener('mousemove', doPan);
128+
svg.addEventListener('mouseup', endPan);
129+
svg.addEventListener('mouseleave', endPan);
130+
svg.addEventListener('wheel', zoom, { passive: false });
131+
svg.addEventListener('dblclick', doubleClickZoom);
132+
svg.addEventListener('touchstart', (event) => {
133+
event.preventDefault();
134+
startPan(event);
135+
}, { passive: false });
136+
svg.addEventListener('touchmove', (event) => {
137+
event.preventDefault();
138+
doPan(event);
139+
}, { passive: false });
140+
svg.addEventListener('touchend', endPan);
141+
});
142+
143+
144+
onUnmounted(() => {
145+
const svg = svgRef.value;
146+
if (!svg) return;
147+
svg.removeEventListener('mousedown', startPan);
148+
svg.removeEventListener('mousemove', doPan);
149+
svg.removeEventListener('mouseup', endPan);
150+
svg.removeEventListener('mouseleave', endPan);
151+
svg.removeEventListener('wheel', zoom);
152+
svg.removeEventListener('dblclick', doubleClickZoom);
153+
svg.removeEventListener('touchstart', startPan);
154+
svg.removeEventListener('touchmove', doPan);
155+
svg.removeEventListener('touchend', endPan);
156+
});
157+
158+
return { viewBox };
159+
}

0 commit comments

Comments
 (0)