From 61d80e5aca1efcf916ab637623848b3543495ed7 Mon Sep 17 00:00:00 2001 From: dax Date: Tue, 4 Aug 2026 08:18:11 +0000 Subject: pathways: mouse-gesture zoom - cursor-anchored wheel/pinch zoom with min/max clamps, keeps zoom across navigation, 0/f/double-click resets to fit --- public/app.js | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) (limited to 'public/app.js') diff --git a/public/app.js b/public/app.js index 6c69f92..2797347 100644 --- a/public/app.js +++ b/public/app.js @@ -8,6 +8,8 @@ const startBtn = $("#start") const GAP = 10 const FIT_TOLERANCE = 350 +const MIN_ZOOM = 0.05 +const MAX_ZOOM = 5 const ARROW_SVG = { N: '', E: '', @@ -25,6 +27,7 @@ let cur = null let panX = 0 let panY = 0 let scale = 1 +let userZoomed = false let drag = null function aspect(info) { @@ -59,6 +62,24 @@ function center() { applyPan() } +function zoomAt(mx, my, factor) { + const ns = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, scale * factor)) + if (ns === scale) return + const wx = (mx - panX) / scale + const wy = (my - panY) / scale + panX = mx - wx * ns + panY = my - wy * ns + scale = ns + userZoomed = true + applyPan() +} + +function resetZoom() { + userZoomed = false + scale = worldScale() + center() +} + function overlaps(a, b) { return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y } @@ -168,7 +189,7 @@ function setCurrent(id, rect) { for (const t of tiles.values()) t.el.classList.remove("current") cur = { id, rect } tiles.get(id).el.classList.add("current") - scale = worldScale() + if (!userZoomed) scale = worldScale() center() getExits(id) } @@ -290,12 +311,36 @@ stage.addEventListener("pointerup", (e) => { setCurrent(id, tiles.get(id).rect) }) +let zoomTimer = 0 +stage.addEventListener("wheel", (e) => { + e.preventDefault() + const rect = stage.getBoundingClientRect() + const mx = e.clientX - rect.left + const my = e.clientY - rect.top + let dy = e.deltaY + if (e.deltaMode === 1) dy *= 16 + else if (e.deltaMode === 2) dy *= window.innerHeight + const factor = Math.exp(-dy * (e.ctrlKey ? 0.012 : 0.0018)) + world.style.transition = "none" + zoomAt(mx, my, factor) + clearTimeout(zoomTimer) + zoomTimer = setTimeout(() => { world.style.transition = "" }, 120) +}, { passive: false }) + +stage.addEventListener("dblclick", (e) => { + if (e.target.closest(".tile")) return + resetZoom() +}) + addEventListener("keydown", (e) => { const map = { ArrowUp: "N", ArrowRight: "E", ArrowDown: "S", ArrowLeft: "W" } const dir = map[e.key] if (dir) { e.preventDefault() if (cur) moveFrom(cur.id, dir) + } else if (e.key === "0" || e.key === "f" || e.key === "F") { + e.preventDefault() + resetZoom() } }) -- cgit v1.3.1