Warp travel shimmers at the threshold, not around the traveler

Green rings at the traveler's squares become a portal curtain: the
opening LINE itself shimmers on both boards — resolved from the warp
table via the step's direction — in iridescent cyan over a soft
violet breath, dashes drifting like light through a veil. Dimensional
warp tokens, whose mouths are whole squares, wrap in the same veil
around the square instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-17 09:11:29 -04:00
co-authored by Claude Fable 5
parent 68b0da6301
commit a82b25b0e8
2 changed files with 58 additions and 17 deletions
+42 -12
View File
@@ -662,10 +662,16 @@
{@const c = center(fx.at)} {@const c = center(fx.at)}
<rect x={c.x - 9} y={c.y - 13} width="18" height="26" rx="2" class="fx-absorb" <rect x={c.x - 9} y={c.y - 13} width="18" height="26" rx="2" class="fx-absorb"
style={`transform-origin: ${c.x}px ${c.y}px`} /> style={`transform-origin: ${c.x}px ${c.y}px`} />
{:else if fx.kind === "warp-glow"} {:else if fx.kind === "portal"}
{@const c = center(fx.at)} {@const x1 = fx.side === "E" ? (fx.cell.x + 1) * CELL : fx.cell.x * CELL}
<circle cx={c.x} cy={c.y} r="8" class="fx-warpglow" /> {@const y1 = fx.side === "S" ? (fx.cell.y + 1) * CELL : fx.cell.y * CELL}
<circle cx={c.x} cy={c.y} r="8" class="fx-warpglow late" /> {@const x2 = fx.side === "W" ? fx.cell.x * CELL : (fx.cell.x + 1) * CELL}
{@const y2 = fx.side === "N" ? fx.cell.y * CELL : (fx.cell.y + 1) * CELL}
<line {x1} {y1} {x2} {y2} class="fx-portal glow" />
<line {x1} {y1} {x2} {y2} class="fx-portal" />
{:else if fx.kind === "portal-cell"}
<rect x={fx.at.x * CELL + 3} y={fx.at.y * CELL + 3}
width={CELL - 6} height={CELL - 6} rx="6" class="fx-portal-veil" />
{:else if fx.kind === "streak"} {:else if fx.kind === "streak"}
{@const a = center(fx.from)} {@const a = center(fx.from)}
{@const b = center(fx.to)} {@const b = center(fx.to)}
@@ -989,16 +995,40 @@
stroke-width: 1.5; stroke-width: 1.5;
animation: fx-swallow 0.65s ease-in forwards; animation: fx-swallow 0.65s ease-in forwards;
} }
.fx-warpglow { .fx-portal {
transform-box: fill-box; stroke: #9be3e0;
transform-origin: center;
fill: none;
stroke: #2e7d32;
stroke-width: 3; stroke-width: 3;
filter: drop-shadow(0 0 5px rgba(46, 125, 50, 0.8)); stroke-dasharray: 6 5;
animation: fx-ring 0.55s ease-out forwards; stroke-linecap: round;
filter: drop-shadow(0 0 4px rgba(155, 227, 224, 0.9));
animation: fx-portal-shimmer 0.95s ease-in-out forwards;
}
.fx-portal.glow {
stroke: rgba(180, 138, 224, 0.5);
stroke-width: 9;
stroke-dasharray: none;
filter: blur(2px);
animation: fx-portal-breathe 0.95s ease-in-out forwards;
}
.fx-portal-veil {
fill: rgba(155, 227, 224, 0.12);
stroke: #9be3e0;
stroke-width: 2.5;
stroke-dasharray: 7 5;
filter: drop-shadow(0 0 4px rgba(155, 227, 224, 0.8));
animation: fx-portal-shimmer 0.95s ease-in-out forwards;
}
@keyframes fx-portal-shimmer {
0% { opacity: 0; stroke-dashoffset: 0; }
20% { opacity: 1; }
80% { opacity: 0.85; }
100% { opacity: 0; stroke-dashoffset: 34; }
}
@keyframes fx-portal-breathe {
0% { opacity: 0; }
30% { opacity: 0.8; }
100% { opacity: 0; }
} }
.fx-warpglow.late { stroke: #7ac47e; animation-delay: 0.15s; opacity: 0; }
.fx-streak { .fx-streak {
stroke: rgba(233, 225, 203, 0.85); stroke: rgba(233, 225, 203, 0.85);
stroke-width: 5; stroke-width: 5;
+16 -5
View File
@@ -10,8 +10,9 @@ type Side = "N" | "E" | "S" | "W";
type FxShape = type FxShape =
| { kind: "fireball" | "bolt" | "waterbolt" | "streak"; from: Cell; to: Cell } | { kind: "fireball" | "bolt" | "waterbolt" | "streak"; from: Cell; to: Cell }
| { kind: "burst" | "splash" | "shimmer" | "shield" | "sparkle" | "whiff" | "hit" | { kind: "burst" | "splash" | "shimmer" | "shield" | "sparkle" | "whiff" | "hit"
| "pow" | "claw" | "absorb" | "warp-glow" | "soul" | "fireworks" | "chaos-swirl" | "pow" | "claw" | "absorb" | "portal-cell" | "soul" | "fireworks" | "chaos-swirl"
| "pit-fall" | "ooze-slip" | "tacks-ow" | "thorn-snap" | "slime-stuck" | "dust-puff"; at: Cell } | "pit-fall" | "ooze-slip" | "tacks-ow" | "thorn-snap" | "slime-stuck" | "dust-puff"; at: Cell }
| { kind: "portal"; cell: Cell; side: Side }
| { kind: "sector-spin"; origin: Cell; clockwise: boolean } | { kind: "sector-spin"; origin: Cell; clockwise: boolean }
| { kind: "sector-slide"; from: Cell; to: Cell } | { kind: "sector-slide"; from: Cell; to: Cell }
| { kind: "edge-dust"; cell: Cell; side: Side }; | { kind: "edge-dust"; cell: Cell; side: Side };
@@ -24,6 +25,7 @@ export function fxTtl(kind: BoardFx["kind"]): number {
switch (kind) { switch (kind) {
case "fireball": case "waterbolt": return 700; case "fireball": case "waterbolt": return 700;
case "bolt": case "streak": return 600; case "bolt": case "streak": return 600;
case "portal": case "portal-cell":
case "soul": case "fireworks": case "chaos-swirl": case "soul": case "fireworks": case "chaos-swirl":
case "sector-spin": case "sector-slide": return 1500; case "sector-spin": case "sector-slide": return 1500;
default: return 900; default: return 900;
@@ -131,14 +133,23 @@ export function fxForEvents(
break; break;
case "moved": case "moved":
if (e.via === "warp") { if (e.via === "warp") {
push({ kind: "warp-glow", at: e.from }); // The mouths are edges: shimmer the opening lines on both boards.
push({ kind: "warp-glow", at: e.to }, 150); const w = view.board.warps.find((w) =>
w.from.cell.x === e.from.x && w.from.cell.y === e.from.y && w.from.side === e.direction);
if (w) {
push({ kind: "portal", cell: w.from.cell, side: w.from.side });
push({ kind: "portal", cell: w.to.cell, side: w.to.side }, 150);
} else {
push({ kind: "portal-cell", at: e.from });
push({ kind: "portal-cell", at: e.to }, 150);
}
beat++; beat++;
} }
break; break;
case "warpStepped": case "warpStepped":
push({ kind: "warp-glow", at: e.from }); // Dimensional warp tokens fill their squares; the veil wraps them.
push({ kind: "warp-glow", at: e.to }, 150); push({ kind: "portal-cell", at: e.from });
push({ kind: "portal-cell", at: e.to }, 150);
beat++; beat++;
break; break;
case "wallCreated": case "wallCreated":