diff options
| author | dax <me@dax.ist> | 2026-08-05 11:12:08 +0100 |
|---|---|---|
| committer | dax <me@dax.ist> | 2026-08-05 11:12:08 +0100 |
| commit | 1442ad4fc3bd52d25ede9a53046d9197bf174107 (patch) | |
| tree | eaebcb83b8a967c5e1a23bfcd95cc266aa585077 | |
| parent | d93aa7dcec19f9a4241eb5a29c4225f49d0bdb15 (diff) | |
touch: tap any visible slot to place, single ghost shown via scroll reveal
| -rw-r--r-- | public/app.css | 1 | ||||
| -rw-r--r-- | public/app.js | 94 | ||||
| -rw-r--r-- | public/index.html | 4 |
3 files changed, 94 insertions, 5 deletions
diff --git a/public/app.css b/public/app.css index 603019d..0fced0b 100644 --- a/public/app.css +++ b/public/app.css @@ -126,6 +126,7 @@ html, body { -webkit-tap-highlight-color: transparent; -webkit-touch-callout: none; } + #stage .preview { opacity: .8; } .nav-zone-N { height: 25%; } .nav-zone-S { height: 25%; } .nav-zone-E { top: 26%; height: 48%; width: 25%; } diff --git a/public/app.js b/public/app.js index f4bb0cb..0959509 100644 --- a/public/app.js +++ b/public/app.js @@ -210,7 +210,8 @@ function setCurrent(id, rect, recenter = true) { getExits(id) if (prevId && prevId !== id) renderNav(prevId) renderNav(id) - refreshPreviews() + if (TOUCH) refreshTouch() + else refreshPreviews() } function renderNav(id) { @@ -246,7 +247,9 @@ async function getExits(id) { const exits = data.exits || [] exitsCache.set(id, exits) renderNav(id) - if (!drag && !suppressHover && !TOUCH) { + if (TOUCH) { + if (!drag) refreshTouch() + } else if (!drag && !suppressHover) { const s = findSlotAt(lastCursor.wx, lastCursor.wy) if (s) { hoveredSlot = s; refreshPreviews() } } @@ -436,6 +439,86 @@ function findSlotAt(wx, wy) { return best } +function visibleSlots() { + const cx = (window.innerWidth / 2 - panX) / scale + const cy = (window.innerHeight / 2 - panY) / scale + const view = { + x: cx - window.innerWidth / 2 / scale, + y: cy - window.innerHeight / 2 / scale, + w: window.innerWidth / scale, + h: window.innerHeight / scale, + } + const seen = new Set() + const out = [] + for (const t of tiles.values()) { + if (!exitsCache.has(t.info.id)) continue + for (const dir of ["N", "E", "S", "W"]) { + if (adjacentTile(t.info.id, dir)) continue + const p = choosePlacement(t.info.id, dir) + if (!p) continue + const q = p.rect + if (!overlaps(q, view)) continue + const key = Math.round(q.x) + "," + Math.round(q.y) + if (seen.has(key)) continue + seen.add(key) + out.push({ id: t.info.id, dir, rect: q }) + } + } + out.sort((a, b) => { + const da = Math.hypot(a.rect.x + a.rect.w / 2 - cx, a.rect.y + a.rect.h / 2 - cy) + const db = Math.hypot(b.rect.x + b.rect.w / 2 - cx, b.rect.y + b.rect.h / 2 - cy) + return da - db + }) + return out +} + +let touchZones = [] + +function hideTouchZones() { + for (const z of touchZones) if (z.parentNode) z.parentNode.removeChild(z) + touchZones = [] +} + +function refreshTouch() { + if (pointers.size > 0) return + hideTouchZones() + const cx = (window.innerWidth / 2 - panX) / scale + const cy = (window.innerHeight / 2 - panY) / scale + const view = { + x: cx - window.innerWidth / 2 / scale, + y: cy - window.innerHeight / 2 / scale, + w: window.innerWidth / scale, + h: window.innerHeight / scale, + } + for (const t of tiles.values()) { + if (!exitsCache.has(t.info.id) && overlaps(view, t.rect)) getExits(t.info.id) + } + const slots = visibleSlots() + const best = slots[0] + if (!best) { + hoveredSlot = null + refreshPreviews() + return + } + const bestKey = Math.round(best.rect.x) + "," + Math.round(best.rect.y) + for (const slot of slots) { + const key = Math.round(slot.rect.x) + "," + Math.round(slot.rect.y) + if (key === bestKey) continue + const z = document.createElement("div") + z.className = "dropzone" + z.style.left = slot.rect.x + "px" + z.style.top = slot.rect.y + "px" + z.style.width = slot.rect.w + "px" + z.style.height = slot.rect.h + "px" + z.addEventListener("pointerdown", (e) => e.stopPropagation()) + z.addEventListener("click", () => { traverseFrom(slot.id, slot.dir); if (TOUCH) refreshTouch() }) + world.appendChild(z) + touchZones.push(z) + } + hoveredSlot = best + refreshPreviews() +} + let lastFollow = 0 let followEnabled = true function followCurrent() { @@ -487,6 +570,7 @@ stage.addEventListener("pointerdown", (e) => { hoveredSlot = null suppressHover = true hideSlot() + hideTouchZones() world.style.transition = "none" if (pointers.size === 2) { followEnabled = true @@ -552,11 +636,15 @@ stage.addEventListener("pointerup", (e) => { if (wasDrag || !tile) { if (wasDrag && followEnabled) followCurrent() if (!wasDrag && !tile && e.pointerType === "touch") bgTap() + if (TOUCH) refreshTouch() return } const id = Number(tile.dataset.id) followEnabled = false - if (cur && cur.id === id) return + if (cur && cur.id === id) { + if (TOUCH) refreshTouch() + return + } setCurrent(id, tiles.get(id).rect) }) diff --git a/public/index.html b/public/index.html index dcd66b4..9a6d66a 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=64"> + <link rel="stylesheet" href="/app.css?v=65"> </head> <body> <main id="stage"> @@ -34,6 +34,6 @@ <span id="progress-text"></span> </div> <div id="toast" role="status" aria-live="polite"></div> - <script src="/app.js?v=64"></script> + <script src="/app.js?v=65"></script> </body> </html> |
