From 51847eceda8057c2e6daaf394cf337f231b750b2 Mon Sep 17 00:00:00 2001 From: dax Date: Tue, 4 Aug 2026 22:12:43 +0100 Subject: single follow-preview: hover brightens to .85, persists at last option dimmed to .35; drop ghost + nav-edge buttons on current card --- public/app.css | 19 ++------ public/app.js | 143 +++++++++++++++++++----------------------------------- public/index.html | 4 +- 3 files changed, 56 insertions(+), 110 deletions(-) (limited to 'public') diff --git a/public/app.css b/public/app.css index bbc2655..fd7612e 100644 --- a/public/app.css +++ b/public/app.css @@ -48,26 +48,17 @@ html, body { } .tile img { width: 100%; height: 100%; object-fit: cover; display: block; border-radius: 6px; } .tile.lock { animation: lock .5s cubic-bezier(.22, 1, .36, 1); } -.tile.ghost { - opacity: .5; - pointer-events: none; - box-shadow: 0 0 0 2px rgba(125, 211, 252, .65); -} .dropzone { position: absolute; cursor: pointer; } -.dropzone.hidden { visibility: hidden; } -.dropzone .preview.hide { visibility: hidden; } -.dropzone .preview { - width: 100%; - height: 100%; - opacity: .5; +.preview { + position: absolute; + opacity: .35; 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; } -.dropzone .preview img { +.preview.hot { opacity: .85; } +.preview img { width: 100%; height: 100%; object-fit: cover; diff --git a/public/app.js b/public/app.js index cab053e..f323b22 100644 --- a/public/app.js +++ b/public/app.js @@ -210,30 +210,15 @@ function renderNav(id) { t.el.querySelectorAll(".nav").forEach((n) => n.remove()) const exits = exitsCache.get(id) if (!exits) return - const isCurrent = cur && cur.id === id - if (TOUCH && !isCurrent) return + if (cur && cur.id === id) return + if (TOUCH) return const nav = document.createElement("div") nav.className = "nav" for (const ex of exits) { const zone = document.createElement("div") zone.className = `nav-zone nav-zone-${ex.dir}` - if (!TOUCH) { - zone.addEventListener("pointermove", () => { - if (adjacentTile(id, ex.dir)) return - cancelHideGhost() - showGhost(choosePlacement(id, ex.dir)) - }) - zone.addEventListener("pointerleave", () => { - scheduleHideGhost() - }) - } zone.addEventListener("pointerdown", (e) => e.stopPropagation()) zone.addEventListener("click", (e) => { e.stopPropagation(); traverseFrom(id, ex.dir) }) - if (!adjacentTile(id, ex.dir) && isCurrent) { - const edge = document.createElement("div") - edge.className = `nav-edge nav-edge-${ex.dir}` - zone.appendChild(edge) - } nav.appendChild(zone) } t.el.appendChild(nav) @@ -341,70 +326,21 @@ function traverseFrom(id, dir) { followEnabled = false 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) } -let ghost = null -let ghostKey = "" -let ghostVisible = false -function setPreviewHidden(hidden) { - ghostVisible = hidden - if (!cur) return - const entry = zones.get(cur.id) - if (entry) entry.prevEl.classList.toggle("hide", hidden) -} -function showGhost(p) { - if (!p) return - const key = p.id + "|" + p.rect.x + "," + p.rect.y - if (ghost && ghostKey === key) return - hideGhost() - ghostKey = key - const el = document.createElement("div") - el.className = "tile ghost" - el.style.left = p.rect.x + "px" - el.style.top = p.rect.y + "px" - el.style.width = p.rect.w + "px" - el.style.height = p.rect.h + "px" - const img = document.createElement("img") - img.src = p.info.thumb - img.alt = "" - el.appendChild(img) - world.appendChild(el) - ghost = el - setPreviewHidden(true) -} - -function hideGhost() { - if (ghost) { - world.removeChild(ghost) - ghost = null - } - ghostKey = "" - setPreviewHidden(false) -} - -let hideGhostTimer = 0 -function scheduleHideGhost() { - clearTimeout(hideGhostTimer) - hideGhostTimer = setTimeout(hideGhost, 80) -} -function cancelHideGhost() { - clearTimeout(hideGhostTimer) -} - const zones = new Map() function clearTileZones(id) { const entry = zones.get(id) if (entry) { for (const z of entry.els) world.removeChild(z) + if (entry.preview) world.removeChild(entry.preview) zones.delete(id) } } @@ -413,6 +349,12 @@ function clearAllZones() { for (const id of [...zones.keys()]) clearTileZones(id) } +function resetPreview() { + if (!cur) return + const entry = zones.get(cur.id) + if (entry && entry.reset) entry.reset() +} + function renderTileZones(id) { if (!cur || id !== cur.id) return if (!tiles.has(id) || !exitsCache.has(id)) return @@ -429,28 +371,30 @@ function renderTileZones(id) { 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) + if (existing && existing.key === key) return clearTileZones(id) if (!best) return const els = [] - const prevEl = document.createElement("div") - prevEl.className = "preview" - const img = document.createElement("img") - img.src = best.p.info.thumb - img.alt = "" - img.loading = "lazy" - prevEl.appendChild(img) + const preview = document.createElement("div") + preview.className = "preview" + const previewImg = document.createElement("img") + previewImg.alt = "" + previewImg.loading = "lazy" + preview.appendChild(previewImg) + let hotDir = null + const showAt = (x) => { + preview.style.left = x.p.rect.x + "px" + preview.style.top = x.p.rect.y + "px" + preview.style.width = x.p.rect.w + "px" + preview.style.height = x.p.rect.h + "px" + previewImg.src = x.p.info.thumb + } + const reset = () => { + hotDir = null + preview.classList.remove("hot") + if (best) showAt(best) + } for (const x of dirs) { const z = document.createElement("div") z.className = "dropzone" @@ -461,17 +405,27 @@ function renderTileZones(id) { 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()) + if (!TOUCH) { + z.addEventListener("pointermove", () => { + if (hotDir !== x.dir) { + hotDir = x.dir + showAt(x) + preview.classList.add("hot") + } + }) + z.addEventListener("pointerleave", () => { + if (hotDir === x.dir) { + hotDir = null + preview.classList.remove("hot") + } + }) } 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) + world.appendChild(preview) + showAt(best) + zones.set(id, { els, key, preview, reset }) } function refreshPreviews() { @@ -528,7 +482,7 @@ stage.addEventListener("pointerdown", (e) => { if (e.target.closest("#import-state")) return pointers.set(e.pointerId, { x: e.clientX, y: e.clientY }) try { stage.setPointerCapture(e.pointerId) } catch { /* ignore */ } - hideGhost() + resetPreview() world.style.transition = "none" if (pointers.size === 2) { followEnabled = true @@ -621,6 +575,7 @@ stage.addEventListener("wheel", (e) => { const rect = stage.getBoundingClientRect() zoomAt(e.clientX - rect.left, e.clientY - rect.top, Math.exp(-dy * 0.01)) } else { + resetPreview() panX -= dx panY -= dy applyPan() diff --git a/public/index.html b/public/index.html index 1fab56b..b31e8ce 100644 --- a/public/index.html +++ b/public/index.html @@ -5,7 +5,7 @@ pathways - +
@@ -29,6 +29,6 @@
- + -- cgit v1.3.1