From 6a0dc37b2e5580987df8fd11becdff53b32c9836 Mon Sep 17 00:00:00 2001 From: dax Date: Tue, 4 Aug 2026 11:51:26 +0000 Subject: 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. --- public/app.js | 77 +++++++++++++++++++++++++++++++++---------------------- public/index.html | 2 +- 2 files changed, 48 insertions(+), 31 deletions(-) (limited to 'public') 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 @@

four paths, one per direction. walk them.

- + -- cgit v1.3.1