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.css | 1 + public/app.js | 47 ++++++++++++++++++++++++++++++++++++++++++++++- public/index.html | 6 +++--- 3 files changed, 50 insertions(+), 4 deletions(-) (limited to 'public') diff --git a/public/app.css b/public/app.css index cf44f2a..ceac2aa 100644 --- a/public/app.css +++ b/public/app.css @@ -113,6 +113,7 @@ html, body { #splash h1 { margin: 0; font-size: 44px; font-weight: 300; letter-spacing: .18em; } #splash h1 .dot { color: var(--ring); } #splash p { max-width: 34ch; color: var(--muted); margin: 0; font-size: 15px; line-height: 1.6; } +#splash p .hint { font-size: 12px; letter-spacing: .02em; opacity: .75; } #start { margin-top: 10px; padding: 10px 34px; 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() } }) diff --git a/public/index.html b/public/index.html index 1a4f67d..8aa5ed7 100644 --- a/public/index.html +++ b/public/index.html @@ -5,7 +5,7 @@ pathways - +
@@ -13,9 +13,9 @@

pathways.

-

four paths, one per direction. walk them.

+

four paths, one per direction. walk them.
drag to pan · scroll to zoom · double-click empty space or press 0 to fit

- + -- cgit v1.3.1