modal store loads image detail (with neighbors) on open, exposes goPrev/ goNext that re-fetch the next image, and implements optimistic tag remove with rollback on API failure. createAndAdd is the bridge between the autocomplete UI and a brand-new tag. usePanZoom is a small composable: click-to-toggle 2x zoom centered on click point, wheel-to-zoom up to 4x, drag-to-pan when zoomed. Pointer events are used (touch + mouse + pen unified). Caller spreads handlers onto the element it wants pannable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
82 lines
1.9 KiB
JavaScript
82 lines
1.9 KiB
JavaScript
// 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
|
|
}
|
|
}
|