diff options
Diffstat (limited to 'public')
| -rw-r--r-- | public/app.css | 13 | ||||
| -rw-r--r-- | public/app.js | 65 | ||||
| -rw-r--r-- | public/index.html | 4 |
3 files changed, 66 insertions, 16 deletions
diff --git a/public/app.css b/public/app.css index 6d5e992..5640b1c 100644 --- a/public/app.css +++ b/public/app.css @@ -49,7 +49,18 @@ html, body { pointer-events: none; box-shadow: 0 0 0 2px rgba(125, 211, 252, .65); } -.dropzone { position: absolute; } +.dropzone { position: absolute; cursor: pointer; } +.dropzone .preview { + width: 100%; + height: 100%; + opacity: .5; + pointer-events: none; + border-radius: 6px; + box-shadow: 0 0 0 2px rgba(125, 211, 252, .5); + transition: opacity .15s; +} +.dropzone:hover .preview, +.dropzone:active .preview { opacity: .85; } @keyframes lock { 0% { transform: scale(.2); opacity: 0; } diff --git a/public/app.js b/public/app.js index 1c920ae..21b3e49 100644 --- a/public/app.js +++ b/public/app.js @@ -11,7 +11,6 @@ const FIT_TOLERANCE = 350 const MIN_ZOOM = 0.05 const MAX_ZOOM = 5 const EPS = 0.5 -const MAX_ZONE_TILES = 24 let U = 300 const tiles = new Map() @@ -180,16 +179,19 @@ function addTile(info, rect) { return el } -function setCurrent(id, rect) { +function setCurrent(id, rect, recenter = true) { const prevId = cur && cur.id for (const t of tiles.values()) t.el.classList.remove("current") cur = { id, rect } tiles.get(id).el.classList.add("current") - if (!userZoomed) scale = worldScale() - center() + if (recenter) { + if (!userZoomed) scale = worldScale() + center() + } getExits(id) if (prevId && prevId !== id) renderNav(prevId) renderNav(id) + refreshPreviews() } function renderNav(id) { @@ -208,6 +210,7 @@ function renderNav(id) { if (!TOUCH) { zone.addEventListener("pointermove", () => { if (adjacentTile(id, ex.dir)) return + if (cur && cur.id === id) return cancelHideGhost() showGhost(choosePlacement(id, ex.dir)) }) @@ -337,7 +340,6 @@ function traverseFrom(id, dir) { hideGhost() addTile(chosen.info, chosen.rect) setCurrent(chosen.id, chosen.rect) - refreshAllZones() } let ghost = null @@ -394,7 +396,7 @@ function clearAllZones() { function renderTileZones(id) { clearTileZones(id) - if (TOUCH) return + if (!cur || id !== cur.id) return if (!tiles.has(id) || !exitsCache.has(id)) return const els = [] for (const dir of ["N", "E", "S", "W"]) { @@ -407,8 +409,14 @@ function renderTileZones(id) { z.style.top = p.rect.y + "px" z.style.width = p.rect.w + "px" z.style.height = p.rect.h + "px" - z.addEventListener("pointermove", () => { cancelHideGhost(); showGhost(p) }) - z.addEventListener("pointerleave", () => scheduleHideGhost()) + const prev = document.createElement("div") + prev.className = "preview" + const img = document.createElement("img") + img.src = p.info.thumb + img.alt = "" + img.loading = "lazy" + prev.appendChild(img) + z.appendChild(prev) z.addEventListener("pointerdown", (e) => e.stopPropagation()) z.addEventListener("click", () => traverseFrom(id, dir)) world.appendChild(z) @@ -417,13 +425,41 @@ function renderTileZones(id) { if (els.length) zones.set(id, els) } -function refreshAllZones() { +function refreshPreviews() { clearAllZones() - if (tiles.size > MAX_ZONE_TILES && cur) { - renderTileZones(cur.id) - return + if (cur) renderTileZones(cur.id) +} + +let lastFollow = 0 +function followCurrent() { + if (!tiles.size || !cur) return + 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, + } + let best = null + let bestD = Infinity + for (const t of tiles.values()) { + const r = t.rect + if (!overlaps(view, r)) continue + const px = Math.max(r.x, Math.min(cx, r.x + r.w)) + const py = Math.max(r.y, Math.min(cy, r.y + r.h)) + const d = Math.hypot(cx - px, cy - py) + if (d < bestD) { bestD = d; best = t } } - for (const id of tiles.keys()) if (exitsCache.has(id)) renderTileZones(id) + if (!best || best.info.id === cur.id) return + setCurrent(best.info.id, best.rect, false) +} + +function maybeFollow() { + const now = performance.now() + if (now - lastFollow < 200) return + lastFollow = now + followCurrent() } stage.addEventListener("pointerover", (e) => { @@ -470,6 +506,7 @@ stage.addEventListener("pointermove", (e) => { panX = drag.px + dx panY = drag.py + dy applyPan() + maybeFollow() } }) @@ -487,6 +524,7 @@ stage.addEventListener("pointerup", (e) => { drag = null world.style.transition = "" if (wasDrag || !tile) { + if (wasDrag) followCurrent() if (!wasDrag && !tile && e.pointerType === "touch") bgTap() return } @@ -528,6 +566,7 @@ stage.addEventListener("wheel", (e) => { panX -= dx panY -= dy applyPan() + maybeFollow() } clearTimeout(zoomTimer) zoomTimer = setTimeout(() => { world.style.transition = "" }, 120) diff --git a/public/index.html b/public/index.html index 0beff50..1a30ce1 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=21"> + <link rel="stylesheet" href="/app.css?v=22"> </head> <body> <main id="stage"> @@ -16,6 +16,6 @@ <p>four paths, one per direction. walk them.</p> <button id="start">start</button> </div> - <script src="/app.js?v=21"></script> + <script src="/app.js?v=22"></script> </body> </html> |
