aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordax <me@dax.ist>2026-08-05 11:12:08 +0100
committerdax <me@dax.ist>2026-08-05 11:12:08 +0100
commit1442ad4fc3bd52d25ede9a53046d9197bf174107 (patch)
treeeaebcb83b8a967c5e1a23bfcd95cc266aa585077
parentd93aa7dcec19f9a4241eb5a29c4225f49d0bdb15 (diff)
touch: tap any visible slot to place, single ghost shown via scroll reveal
-rw-r--r--public/app.css1
-rw-r--r--public/app.js94
-rw-r--r--public/index.html4
3 files changed, 94 insertions, 5 deletions
diff --git a/public/app.css b/public/app.css
index 603019d..0fced0b 100644
--- a/public/app.css
+++ b/public/app.css
@@ -126,6 +126,7 @@ html, body {
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
}
+ #stage .preview { opacity: .8; }
.nav-zone-N { height: 25%; }
.nav-zone-S { height: 25%; }
.nav-zone-E { top: 26%; height: 48%; width: 25%; }
diff --git a/public/app.js b/public/app.js
index f4bb0cb..0959509 100644
--- a/public/app.js
+++ b/public/app.js
@@ -210,7 +210,8 @@ function setCurrent(id, rect, recenter = true) {
getExits(id)
if (prevId && prevId !== id) renderNav(prevId)
renderNav(id)
- refreshPreviews()
+ if (TOUCH) refreshTouch()
+ else refreshPreviews()
}
function renderNav(id) {
@@ -246,7 +247,9 @@ async function getExits(id) {
const exits = data.exits || []
exitsCache.set(id, exits)
renderNav(id)
- if (!drag && !suppressHover && !TOUCH) {
+ if (TOUCH) {
+ if (!drag) refreshTouch()
+ } else if (!drag && !suppressHover) {
const s = findSlotAt(lastCursor.wx, lastCursor.wy)
if (s) { hoveredSlot = s; refreshPreviews() }
}
@@ -436,6 +439,86 @@ function findSlotAt(wx, wy) {
return best
}
+function visibleSlots() {
+ 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,
+ }
+ const seen = new Set()
+ const out = []
+ for (const t of tiles.values()) {
+ if (!exitsCache.has(t.info.id)) continue
+ for (const dir of ["N", "E", "S", "W"]) {
+ if (adjacentTile(t.info.id, dir)) continue
+ const p = choosePlacement(t.info.id, dir)
+ if (!p) continue
+ const q = p.rect
+ if (!overlaps(q, view)) continue
+ const key = Math.round(q.x) + "," + Math.round(q.y)
+ if (seen.has(key)) continue
+ seen.add(key)
+ out.push({ id: t.info.id, dir, rect: q })
+ }
+ }
+ out.sort((a, b) => {
+ const da = Math.hypot(a.rect.x + a.rect.w / 2 - cx, a.rect.y + a.rect.h / 2 - cy)
+ const db = Math.hypot(b.rect.x + b.rect.w / 2 - cx, b.rect.y + b.rect.h / 2 - cy)
+ return da - db
+ })
+ return out
+}
+
+let touchZones = []
+
+function hideTouchZones() {
+ for (const z of touchZones) if (z.parentNode) z.parentNode.removeChild(z)
+ touchZones = []
+}
+
+function refreshTouch() {
+ if (pointers.size > 0) return
+ hideTouchZones()
+ 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,
+ }
+ for (const t of tiles.values()) {
+ if (!exitsCache.has(t.info.id) && overlaps(view, t.rect)) getExits(t.info.id)
+ }
+ const slots = visibleSlots()
+ const best = slots[0]
+ if (!best) {
+ hoveredSlot = null
+ refreshPreviews()
+ return
+ }
+ const bestKey = Math.round(best.rect.x) + "," + Math.round(best.rect.y)
+ for (const slot of slots) {
+ const key = Math.round(slot.rect.x) + "," + Math.round(slot.rect.y)
+ if (key === bestKey) continue
+ const z = document.createElement("div")
+ z.className = "dropzone"
+ z.style.left = slot.rect.x + "px"
+ z.style.top = slot.rect.y + "px"
+ z.style.width = slot.rect.w + "px"
+ z.style.height = slot.rect.h + "px"
+ z.addEventListener("pointerdown", (e) => e.stopPropagation())
+ z.addEventListener("click", () => { traverseFrom(slot.id, slot.dir); if (TOUCH) refreshTouch() })
+ world.appendChild(z)
+ touchZones.push(z)
+ }
+ hoveredSlot = best
+ refreshPreviews()
+}
+
let lastFollow = 0
let followEnabled = true
function followCurrent() {
@@ -487,6 +570,7 @@ stage.addEventListener("pointerdown", (e) => {
hoveredSlot = null
suppressHover = true
hideSlot()
+ hideTouchZones()
world.style.transition = "none"
if (pointers.size === 2) {
followEnabled = true
@@ -552,11 +636,15 @@ stage.addEventListener("pointerup", (e) => {
if (wasDrag || !tile) {
if (wasDrag && followEnabled) followCurrent()
if (!wasDrag && !tile && e.pointerType === "touch") bgTap()
+ if (TOUCH) refreshTouch()
return
}
const id = Number(tile.dataset.id)
followEnabled = false
- if (cur && cur.id === id) return
+ if (cur && cur.id === id) {
+ if (TOUCH) refreshTouch()
+ return
+ }
setCurrent(id, tiles.get(id).rect)
})
diff --git a/public/index.html b/public/index.html
index dcd66b4..9a6d66a 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=64">
+ <link rel="stylesheet" href="/app.css?v=65">
</head>
<body>
<main id="stage">
@@ -34,6 +34,6 @@
<span id="progress-text"></span>
</div>
<div id="toast" role="status" aria-live="polite"></div>
- <script src="/app.js?v=64"></script>
+ <script src="/app.js?v=65"></script>
</body>
</html>