aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md9
-rw-r--r--public/app.css34
-rw-r--r--public/app.js30
3 files changed, 40 insertions, 33 deletions
diff --git a/README.md b/README.md
index 8a0496b..31c6e94 100644
--- a/README.md
+++ b/README.md
@@ -23,10 +23,11 @@ stays behind basic auth at <https://prism.dax.ist>; pathways is public.
- Each path is an ordered cycle (by taken date, then id): walking "grey" goes
grey → grey → … and wraps around. Assignment is deterministic, so revisiting
a photo always offers the same four exits.
-- If a step lands on a photo already placed in the mosaic, focus just jumps to
- its existing tile — the mosaic never holds two copies. If it lands on a grid
- cell already occupied by a *different* photo, the new photo keeps stepping in
- that direction until it finds a free cell, so nothing is ever covered up.
+- Movement is spatial: pressing a direction lands you on whatever photo already
+ occupies that cell — you walk through photos you've placed before rather than
+ over them. New photos are only added when you step into a genuinely empty
+ cell. If the path's next photo is already placed elsewhere, focus jumps to
+ it, so the mosaic never holds two copies of a photo.
## Run it
diff --git a/public/app.css b/public/app.css
index ddd472d..44b7c7f 100644
--- a/public/app.css
+++ b/public/app.css
@@ -37,13 +37,12 @@ html, body {
.tile {
position: absolute;
border-radius: 6px;
- overflow: hidden;
background: var(--tile);
box-shadow: 0 2px 10px rgba(0, 0, 0, .45);
cursor: pointer;
transition: box-shadow .2s;
}
-.tile img { width: 100%; height: 100%; object-fit: cover; display: block; }
+.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); }
@keyframes lock {
@@ -67,20 +66,20 @@ html, body {
.nav-btn {
position: absolute;
pointer-events: auto;
- width: 40px;
- height: 40px;
+ width: 44px;
+ height: 44px;
border-radius: 50%;
- border: 1px solid rgba(255, 255, 255, .25);
- background: rgba(14, 15, 19, .85);
- color: var(--ink);
- font-size: 15px;
- line-height: 1;
+ border: none;
+ background: rgba(255, 255, 255, .95);
+ color: #17181d;
cursor: pointer;
display: grid;
place-items: center;
- transition: transform .12s, border-color .12s;
+ box-shadow: 0 2px 14px rgba(0, 0, 0, .5);
+ transition: transform .12s, background .12s, box-shadow .12s;
}
-.nav-btn:hover { border-color: var(--ring); transform: scale(1.12); }
+.nav-btn svg { width: 22px; height: 22px; display: block; }
+.nav-btn:hover { transform: scale(1.15); background: #fff; box-shadow: 0 4px 18px rgba(0, 0, 0, .55); }
.nav-btn[data-dir="N"] { top: 0; left: 50%; translate: -50% -50%; }
.nav-btn[data-dir="E"] { top: 50%; right: 0; translate: 50% -50%; }
.nav-btn[data-dir="S"] { bottom: 0; left: 50%; translate: -50% 50%; }
@@ -91,13 +90,14 @@ html, body {
left: 50%;
translate: -50%;
white-space: nowrap;
- font-size: 11px;
- letter-spacing: .04em;
+ font-size: 12px;
+ font-weight: 500;
+ letter-spacing: .02em;
color: var(--ink);
- background: rgba(14, 15, 19, .92);
- border: 1px solid rgba(255, 255, 255, .18);
- border-radius: 4px;
- padding: 2px 7px;
+ background: rgba(18, 19, 24, .92);
+ border: 1px solid rgba(255, 255, 255, .14);
+ border-radius: 999px;
+ padding: 3px 10px;
opacity: 0;
transition: opacity .12s;
pointer-events: none;
diff --git a/public/app.js b/public/app.js
index faacc84..8b7c475 100644
--- a/public/app.js
+++ b/public/app.js
@@ -7,9 +7,14 @@ const splash = $("#splash")
const flash = $("#flash")
const startBtn = $("#start")
-const GAP = 8
-let TILE = window.matchMedia("(max-width: 700px)").matches ? 150 : 240
-const ARROW = { N: "↑", E: "→", S: "↓", W: "←" }
+const GAP = 10
+let TILE = window.matchMedia("(max-width: 700px)").matches ? 200 : 360
+const ARROW_SVG = {
+ N: '<svg viewBox="0 0 24 24"><path d="M5 15l7-8 7 8" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
+ E: '<svg viewBox="0 0 24 24"><path d="M9 5l8 7-8 7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
+ S: '<svg viewBox="0 0 24 24"><path d="M5 9l7 8 7-8" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
+ W: '<svg viewBox="0 0 24 24"><path d="M15 5l-8 7 8 7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
+}
const DELTA = { N: [0, -1], E: [1, 0], S: [0, 1], W: [-1, 0] }
const tiles = new Map()
@@ -87,7 +92,7 @@ function renderNav() {
const b = document.createElement("button")
b.className = "nav-btn"
b.dataset.dir = ex.dir
- b.textContent = ARROW[ex.dir]
+ b.innerHTML = ARROW_SVG[ex.dir]
const lab = document.createElement("span")
lab.className = "nav-label"
lab.textContent = ex.path.name
@@ -116,18 +121,19 @@ async function move(dir) {
if (!ex) return
flashText(ex.path.name)
const [dx, dy] = DELTA[dir]
+ const nx = cur.x + dx
+ const ny = cur.y + dy
+ const occupant = tiles.get(key(nx, ny))
+ if (occupant) {
+ setCurrent(occupant.info.id, nx, ny)
+ return
+ }
const at = coordOf.get(ex.next.id)
if (at) {
setCurrent(ex.next.id, at.x, at.y)
} else {
- let px = cur.x + dx
- let py = cur.y + dy
- while (tiles.has(key(px, py))) {
- px += dx
- py += dy
- }
- addTile(ex.next, px, py)
- setCurrent(ex.next.id, px, py)
+ addTile(ex.next, nx, ny)
+ setCurrent(ex.next.id, nx, ny)
}
}