diff options
| author | dax <me@dax.ist> | 2026-08-04 08:18:11 +0000 |
|---|---|---|
| committer | dax <me@dax.ist> | 2026-08-04 08:18:11 +0000 |
| commit | 61d80e5aca1efcf916ab637623848b3543495ed7 (patch) | |
| tree | 21583ed4a18c555715bd1b29c28f497ffbf6a9e8 /public | |
| parent | 2ed3e870b70ab1991e5cda8a89d4c6a583d6f4e3 (diff) | |
pathways: mouse-gesture zoom - cursor-anchored wheel/pinch zoom with min/max clamps, keeps zoom across navigation, 0/f/double-click resets to fit
Diffstat (limited to 'public')
| -rw-r--r-- | public/app.css | 1 | ||||
| -rw-r--r-- | public/app.js | 47 | ||||
| -rw-r--r-- | public/index.html | 6 |
3 files changed, 50 insertions, 4 deletions
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: '<svg viewBox="0 0 24 24"><path d="M5 15l7-8 7 8" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>', E: '<svg viewBox="0 0 24 24"><path d="M9 5l8 7-8 7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>', @@ -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 @@ <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="color-scheme" content="dark"> <title>pathways</title> - <link rel="stylesheet" href="/app.css?v=5"> + <link rel="stylesheet" href="/app.css?v=6"> </head> <body> <main id="stage"> @@ -13,9 +13,9 @@ </main> <div id="splash"> <h1>pathways<span class="dot">.</span></h1> - <p>four paths, one per direction. walk them.</p> + <p>four paths, one per direction. walk them.<br><span class="hint">drag to pan · scroll to zoom · double-click empty space or press 0 to fit</span></p> <button id="start">start</button> </div> - <script src="/app.js?v=5"></script> + <script src="/app.js?v=6"></script> </body> </html> |
