aboutsummaryrefslogtreecommitdiff
path: root/public/app.js
diff options
context:
space:
mode:
authordax <me@dax.ist>2026-08-04 10:38:09 +0000
committerdax <me@dax.ist>2026-08-04 10:38:09 +0000
commit43ec71f91b70fd65e180a4e2f6a4df80dad04b27 (patch)
treec2650afbce8a6343fae53cb0ad3f1a347daf94a2 /public/app.js
parent56b3a602c8e992a2bc197844e1ab3de8743b8012 (diff)
Touch interaction model for coarse pointers, desktop unchanged
Gated on matchMedia("(hover: none)") so mouse/trackpad behavior is untouched: - Nav zones + edge markers only on the current tile; tapping any other tile selects it first. No more accidental traverses on touch. - Edges always visible on touch (the hover glow becomes the tap affordance), glow while pressed via :active. Bands modestly shrunk so tile centers stay grab-to-pan. - No hover ghost preview and no dropzones on touch (they were invisible tap traps without hover). - Two-finger pinch to zoom (pointer-count based, works everywhere). - Double-tap on empty space resets zoom, mirroring desktop dblclick. - Kill iOS tap-highlight and long-press callout. Adds /tmp/touchtest.mjs (15 checks) covering select/traverse/pinch/ double-tap/pan; domtest 52/52, exhaust 9/9, splash 6/6, fuzz clean.
Diffstat (limited to 'public/app.js')
-rw-r--r--public/app.js76
1 files changed, 63 insertions, 13 deletions
diff --git a/public/app.js b/public/app.js
index 95f64d4..1c920ae 100644
--- a/public/app.js
+++ b/public/app.js
@@ -26,6 +26,8 @@ let scale = 1
let userZoomed = false
let drag = null
+const TOUCH = typeof window.matchMedia === "function" && window.matchMedia("(hover: none)").matches
+
function aspect(info) {
return info.h > 0 ? info.w / info.h : 1
}
@@ -197,19 +199,22 @@ function renderNav(id) {
const exits = exitsCache.get(id)
if (!exits) return
const isCurrent = cur && cur.id === id
+ if (TOUCH && !isCurrent) 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}`
- zone.addEventListener("pointermove", () => {
- if (adjacentTile(id, ex.dir)) return
- cancelHideGhost()
- showGhost(choosePlacement(id, ex.dir))
- })
- zone.addEventListener("pointerleave", () => {
- scheduleHideGhost()
- })
+ 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) {
@@ -389,6 +394,7 @@ function clearAllZones() {
function renderTileZones(id) {
clearTileZones(id)
+ if (TOUCH) return
if (!tiles.has(id) || !exitsCache.has(id)) return
const els = []
for (const dir of ["N", "E", "S", "W"]) {
@@ -425,15 +431,37 @@ stage.addEventListener("pointerover", (e) => {
if (tileEl) getExits(Number(tileEl.dataset.id))
})
+const pointers = new Map()
+let pinchDist = 0
+
stage.addEventListener("pointerdown", (e) => {
if (e.target.closest(".nav-zone")) return
+ pointers.set(e.pointerId, { x: e.clientX, y: e.clientY })
+ stage.setPointerCapture(e.pointerId)
hideGhost()
- drag = { x: e.clientX, y: e.clientY, px: panX, py: panY, moved: false, tile: e.target.closest(".tile") }
world.style.transition = "none"
- stage.setPointerCapture(e.pointerId)
+ if (pointers.size === 2) {
+ drag = null
+ const [a, b] = [...pointers.values()]
+ pinchDist = Math.hypot(b.x - a.x, b.y - a.y)
+ return
+ }
+ drag = { x: e.clientX, y: e.clientY, px: panX, py: panY, moved: false, tile: e.target.closest(".tile") }
})
stage.addEventListener("pointermove", (e) => {
+ if (pointers.has(e.pointerId)) {
+ pointers.set(e.pointerId, { x: e.clientX, y: e.clientY })
+ if (pointers.size === 2 && pinchDist > 0) {
+ const [a, b] = [...pointers.values()]
+ const d = Math.hypot(b.x - a.x, b.y - a.y)
+ if (d > 0) {
+ zoomAt((a.x + b.x) / 2, (a.y + b.y) / 2, d / pinchDist)
+ pinchDist = d
+ }
+ return
+ }
+ }
if (!drag) return
const dx = e.clientX - drag.x
const dy = e.clientY - drag.y
@@ -446,23 +474,45 @@ stage.addEventListener("pointermove", (e) => {
})
stage.addEventListener("pointerup", (e) => {
+ const wasPinch = pointers.size >= 2
+ pointers.delete(e.pointerId)
+ if (wasPinch && pointers.size === 1) {
+ const p = [...pointers.values()][0]
+ drag = { x: p.x, y: p.y, px: panX, py: panY, moved: true, tile: null }
+ }
+ pinchDist = 0
if (!drag) return
const wasDrag = drag.moved
const tile = drag.tile
drag = null
world.style.transition = ""
- if (wasDrag || !tile) return
+ if (wasDrag || !tile) {
+ if (!wasDrag && !tile && e.pointerType === "touch") bgTap()
+ return
+ }
const id = Number(tile.dataset.id)
if (cur && cur.id === id) return
setCurrent(id, tiles.get(id).rect)
})
-stage.addEventListener("pointercancel", () => {
- if (!drag) return
+stage.addEventListener("pointercancel", (e) => {
+ pointers.delete(e.pointerId)
+ pinchDist = 0
drag = null
world.style.transition = ""
})
+let lastBgTap = -Infinity
+function bgTap() {
+ const now = performance.now()
+ if (now - lastBgTap < 350) {
+ lastBgTap = 0
+ resetZoom()
+ return
+ }
+ lastBgTap = now
+}
+
let zoomTimer = 0
stage.addEventListener("wheel", (e) => {
e.preventDefault()