aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordax <me@dax.ist>2026-08-04 08:18:11 +0000
committerdax <me@dax.ist>2026-08-04 08:18:11 +0000
commit61d80e5aca1efcf916ab637623848b3543495ed7 (patch)
tree21583ed4a18c555715bd1b29c28f497ffbf6a9e8
parent2ed3e870b70ab1991e5cda8a89d4c6a583d6f4e3 (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
-rw-r--r--public/app.css1
-rw-r--r--public/app.js47
-rw-r--r--public/index.html6
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>