aboutsummaryrefslogtreecommitdiff
path: root/public
diff options
context:
space:
mode:
authordax <me@dax.ist>2026-08-04 11:51:26 +0000
committerdax <me@dax.ist>2026-08-04 11:51:26 +0000
commit6a0dc37b2e5580987df8fd11becdff53b32c9836 (patch)
tree564aa9b45a37a35bb859132fcb249918f3323d39 /public
parent8a744362ff9a454d284b1385a7b30cce98271bfb (diff)
hover black space around mosaic: per-free-dir dropzones + hotspots
Mouse users can now hover the empty black space beside the current tile to preview the photo that would be placed at that free-direction spot. renderTileZones now renders one dropzone per free direction (N/E/S/W): - the most-centered free dir keeps the persistent .preview (thumbnail) - the other free dirs are transparent hover hotspots that showGhost() the destination placement on pointermove and scheduleHideGhost() on leave - stable-element logic via a key string: early-return when unchanged; if only bestDir flips, move the cached prevEl into the new best dropzone and update img.src (no element churn / no flicker) - hotspots skipped on TOUCH (kept for tap-to-walk), persistent preview only - one preview at a time via .dropzone.hidden { visibility:hidden } zones map entries are now { els, key, bestDir, prevEl, img }. Tests green: domtest 58, touchtest 19, exhausttest 9, splashtest 6, fuzz clean, hoverlive 0 stray ghosts. Live at ?v=27.
Diffstat (limited to 'public')
-rw-r--r--public/app.js77
-rw-r--r--public/index.html2
2 files changed, 48 insertions, 31 deletions
diff --git a/public/app.js b/public/app.js
index cce9bc8..11f19b9 100644
--- a/public/app.js
+++ b/public/app.js
@@ -348,8 +348,8 @@ let ghostVisible = false
function setPreviewHidden(hidden) {
ghostVisible = hidden
if (!cur) return
- const zs = zones.get(cur.id)
- if (zs) for (const z of zs) z.classList.toggle("hidden", hidden)
+ const entry = zones.get(cur.id)
+ if (entry) for (const z of entry.els) z.classList.toggle("hidden", hidden)
}
function showGhost(p) {
if (!p) return
@@ -392,9 +392,9 @@ function cancelHideGhost() {
const zones = new Map()
function clearTileZones(id) {
- const zs = zones.get(id)
- if (zs) {
- for (const z of zs) world.removeChild(z)
+ const entry = zones.get(id)
+ if (entry) {
+ for (const z of entry.els) world.removeChild(z)
zones.delete(id)
}
}
@@ -408,42 +408,59 @@ function renderTileZones(id) {
if (!tiles.has(id) || !exitsCache.has(id)) return
const cx = (window.innerWidth / 2 - panX) / scale
const cy = (window.innerHeight / 2 - panY) / scale
- let bestDir = null
- let best = null
- let bestD = Infinity
+ const dirs = []
for (const dir of ["N", "E", "S", "W"]) {
if (adjacentTile(id, dir)) continue
const p = choosePlacement(id, dir)
if (!p) continue
const d = Math.hypot(p.rect.x + p.rect.w / 2 - cx, p.rect.y + p.rect.h / 2 - cy)
- if (d < bestD) { bestD = d; bestDir = dir; best = p }
+ dirs.push({ dir, p, d })
+ }
+ const key = dirs.map((x) => x.dir + x.p.info.id + "@" + x.p.rect.x + "," + x.p.rect.y).join("|")
+ let best = null
+ for (const x of dirs) if (!best || x.d < best.d) best = x
+ const entry = zones.get(id)
+ if (entry && entry.key === key) {
+ if (best && best.dir !== entry.bestDir) {
+ const z = entry.els.find((el) => el.dataset.dir === best.dir)
+ if (z) {
+ z.appendChild(entry.prevEl)
+ entry.img.src = best.p.info.thumb
+ entry.bestDir = best.dir
+ }
+ }
+ return
}
- const existing = zones.get(id) && zones.get(id)[0]
- if (best && existing &&
- existing.dataset.dir === bestDir &&
- existing.dataset.key === best.info.id + "|" + best.rect.x + "," + best.rect.y) return
clearTileZones(id)
if (!best) return
- const z = document.createElement("div")
- z.className = "dropzone"
- z.dataset.dir = bestDir
- z.dataset.key = best.info.id + "|" + best.rect.x + "," + best.rect.y
- z.style.left = best.rect.x + "px"
- z.style.top = best.rect.y + "px"
- z.style.width = best.rect.w + "px"
- z.style.height = best.rect.h + "px"
- const prev = document.createElement("div")
- prev.className = "preview"
+ const els = []
+ const prevEl = document.createElement("div")
+ prevEl.className = "preview"
const img = document.createElement("img")
- img.src = best.info.thumb
+ img.src = best.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, bestDir))
- world.appendChild(z)
- zones.set(id, [z])
+ prevEl.appendChild(img)
+ for (const x of dirs) {
+ const z = document.createElement("div")
+ z.className = "dropzone"
+ z.dataset.dir = x.dir
+ z.style.left = x.p.rect.x + "px"
+ z.style.top = x.p.rect.y + "px"
+ z.style.width = x.p.rect.w + "px"
+ z.style.height = x.p.rect.h + "px"
+ z.addEventListener("pointerdown", (e) => e.stopPropagation())
+ z.addEventListener("click", () => traverseFrom(id, x.dir))
+ if (!TOUCH && x.dir !== best.dir) {
+ z.addEventListener("pointermove", () => { cancelHideGhost(); showGhost(x.p) })
+ z.addEventListener("pointerleave", () => scheduleHideGhost())
+ }
+ world.appendChild(z)
+ els.push(z)
+ }
+ const bestEl = els.find((el) => el.dataset.dir === best.dir)
+ bestEl.appendChild(prevEl)
+ zones.set(id, { els, key, bestDir: best.dir, prevEl, img })
setPreviewHidden(ghostVisible)
}
diff --git a/public/index.html b/public/index.html
index 1514600..549ca5d 100644
--- a/public/index.html
+++ b/public/index.html
@@ -16,6 +16,6 @@
<p>four paths, one per direction. walk them.</p>
<button id="start">start</button>
</div>
- <script src="/app.js?v=26"></script>
+ <script src="/app.js?v=27"></script>
</body>
</html>