// Click-to-toggle zoom + drag-to-pan when zoomed + wheel-to-zoom. // Returns reactive state + handlers a component can spread onto its // root element. import { reactive } from 'vue' const MIN_SCALE = 1 const MAX_SCALE = 4 const WHEEL_SENSITIVITY = 0.0015 export function usePanZoom() { const state = reactive({ scale: 1, x: 0, y: 0, panning: false }) let dragOriginX = 0 let dragOriginY = 0 let panOriginX = 0 let panOriginY = 0 function reset() { state.scale = 1 state.x = 0 state.y = 0 } function toggleZoom(ev) { if (state.scale > 1) { reset(); return } state.scale = 2 // Zoom centered on click point if (ev && ev.currentTarget) { const rect = ev.currentTarget.getBoundingClientRect() const cx = ev.clientX - rect.left - rect.width / 2 const cy = ev.clientY - rect.top - rect.height / 2 state.x = -cx state.y = -cy } } function onWheel(ev) { ev.preventDefault() const delta = -ev.deltaY * WHEEL_SENSITIVITY const next = Math.min(MAX_SCALE, Math.max(MIN_SCALE, state.scale + delta * state.scale)) state.scale = next if (next === MIN_SCALE) { state.x = 0; state.y = 0 } } function onPointerDown(ev) { if (state.scale === 1) return state.panning = true dragOriginX = ev.clientX dragOriginY = ev.clientY panOriginX = state.x panOriginY = state.y ev.currentTarget.setPointerCapture(ev.pointerId) } function onPointerMove(ev) { if (!state.panning) return state.x = panOriginX + (ev.clientX - dragOriginX) state.y = panOriginY + (ev.clientY - dragOriginY) } function onPointerUp(ev) { state.panning = false try { ev.currentTarget.releasePointerCapture(ev.pointerId) } catch {} } return { state, handlers: { onWheel, onPointerDown, onPointerMove, onPointerUp, onClick: toggleZoom }, reset } }