diff options
Diffstat (limited to 'public')
| -rw-r--r-- | public/app.js | 81 | ||||
| -rw-r--r-- | public/index.html | 4 |
2 files changed, 67 insertions, 18 deletions
diff --git a/public/app.js b/public/app.js index aa2eef3..e4fa727 100644 --- a/public/app.js +++ b/public/app.js @@ -10,6 +10,8 @@ const GAP = 10 const FIT_TOLERANCE = 350 const MIN_ZOOM = 0.05 const MAX_ZOOM = 5 +const EPS = 0.5 +const MAX_ZONE_TILES = 24 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>', @@ -191,7 +193,6 @@ function setCurrent(id, rect) { tiles.get(id).el.classList.add("current") if (!userZoomed) scale = worldScale() center() - renderDropzones(id) getExits(id) } @@ -211,8 +212,10 @@ function renderNav(id) { b.dataset.dir = ex.dir b.innerHTML = ARROW_SVG[ex.dir] b.addEventListener("pointerdown", (e) => e.stopPropagation()) - b.addEventListener("click", (e) => { e.stopPropagation(); moveFrom(id, ex.dir) }) - b.addEventListener("pointerenter", () => showGhost(choosePlacement(id, ex.dir))) + b.addEventListener("click", (e) => { e.stopPropagation(); traverseFrom(id, ex.dir) }) + b.addEventListener("pointerenter", () => { + if (!adjacentTile(id, ex.dir)) showGhost(choosePlacement(id, ex.dir)) + }) b.addEventListener("pointerleave", () => hideGhost()) zone.appendChild(b) nav.appendChild(zone) @@ -233,7 +236,7 @@ async function getExits(id) { const exits = data.exits || [] exitsCache.set(id, exits) renderNav(id) - if (cur && cur.id === id) renderDropzones(id) + renderTileZones(id) return exits } catch { return null @@ -277,12 +280,40 @@ function choosePlacement(id, dir) { return { id: chosen.id, info: chosen.info, rect: chosen.rect } } -function moveFrom(id, dir) { +function adjacentTile(id, dir) { + const r = tiles.get(id).rect + let best = null + let bestDist = Infinity + for (const t of tiles.values()) { + if (t.info.id === id) continue + const q = t.rect + let adj = false + if (dir === "N") adj = Math.abs(q.y + q.h + GAP - r.y) <= EPS && q.x < r.x + r.w && q.x + q.w > r.x + else if (dir === "S") adj = Math.abs(q.y - (r.y + r.h + GAP)) <= EPS && q.x < r.x + r.w && q.x + q.w > r.x + else if (dir === "E") adj = Math.abs(q.x - (r.x + r.w + GAP)) <= EPS && q.y < r.y + r.h && q.y + q.h > r.y + else adj = Math.abs(q.x + q.w + GAP - r.x) <= EPS && q.y < r.y + r.h && q.y + q.h > r.y + if (!adj) continue + const d = dir === "N" || dir === "S" + ? Math.abs(q.x + q.w / 2 - (r.x + r.w / 2)) + : Math.abs(q.y + q.h / 2 - (r.y + r.h / 2)) + if (d < bestDist) { bestDist = d; best = t } + } + return best ? best.info.id : null +} + +function traverseFrom(id, dir) { + const back = adjacentTile(id, dir) + if (back) { + hideGhost() + setCurrent(back, tiles.get(back).rect) + return + } const chosen = choosePlacement(id, dir) if (!chosen) return hideGhost() addTile(chosen.info, chosen.rect) setCurrent(chosen.id, chosen.rect) + refreshAllZones() } let ghost = null @@ -311,17 +342,25 @@ function hideGhost() { } } -let dropzones = [] -function clearDropzones() { - for (const z of dropzones) world.removeChild(z) - dropzones = [] +const zones = new Map() +function clearTileZones(id) { + const zs = zones.get(id) + if (zs) { + for (const z of zs) world.removeChild(z) + zones.delete(id) + } +} + +function clearAllZones() { + for (const id of [...zones.keys()]) clearTileZones(id) } -function renderDropzones(id) { - clearDropzones() - const t = tiles.get(id) - if (!t) return +function renderTileZones(id) { + clearTileZones(id) + if (!tiles.has(id) || !exitsCache.has(id)) return + const els = [] for (const dir of ["N", "E", "S", "W"]) { + if (adjacentTile(id, dir)) continue const p = choosePlacement(id, dir) if (!p) continue const z = document.createElement("div") @@ -333,10 +372,20 @@ function renderDropzones(id) { z.addEventListener("pointerenter", () => showGhost(p)) z.addEventListener("pointerleave", () => hideGhost()) z.addEventListener("pointerdown", (e) => e.stopPropagation()) - z.addEventListener("click", () => moveFrom(id, dir)) + z.addEventListener("click", () => traverseFrom(id, dir)) world.appendChild(z) - dropzones.push(z) + els.push(z) + } + if (els.length) zones.set(id, els) +} + +function refreshAllZones() { + clearAllZones() + if (tiles.size > MAX_ZONE_TILES && cur) { + renderTileZones(cur.id) + return } + for (const id of tiles.keys()) if (exitsCache.has(id)) renderTileZones(id) } stage.addEventListener("pointerover", (e) => { @@ -407,7 +456,7 @@ addEventListener("keydown", (e) => { const dir = map[e.key] if (dir) { e.preventDefault() - if (cur) moveFrom(cur.id, dir) + if (cur) traverseFrom(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 7f378d3..51e3ef3 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=8"> + <link rel="stylesheet" href="/app.css?v=9"> </head> <body> <main id="stage"> @@ -16,6 +16,6 @@ <p>four paths, one per direction. walk them.<br><span class="hint">drag or scroll to pan · pinch to zoom · double-click empty space or press 0 to fit</span></p> <button id="start">start</button> </div> - <script src="/app.js?v=8"></script> + <script src="/app.js?v=9"></script> </body> </html> |
