Every flourish gets its own file, and a workshop to watch them in

The effects move out of Board.svelte into src/fx-sprites/ — one
component per flourish, markup and animation CSS together, so editing
the fireball means opening Fireball.svelte and nothing else. A
registry (index.ts) maps kind to sprite; Board renders through it.
The event mapping stays in fx.ts.

And a viewer: /?fx opens the Flourish Workshop — every sprite on a
labeled parchment tile, replaying on a 2.2-second beat. Under the dev
server, editing any sprite file hot-reloads the gallery mid-loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-17 09:27:32 -04:00
co-authored by Claude Fable 5
parent 0fcbff4dc6
commit a59ecfa615
31 changed files with 893 additions and 420 deletions
+119
View File
@@ -0,0 +1,119 @@
<script lang="ts">
// The flourish workshop: every effect sprite on a loop, labeled, against a
// mini-maze. Open with /?fx while `npm run dev` runs — edits to any sprite
// file hot-reload here mid-animation. Not linked from anywhere; harmless.
import { FX_SPRITES } from "./fx-sprites";
import type { BoardFx } from "./fx";
let tick = $state(0);
$effect(() => {
const t = setInterval(() => (tick += 1), 2200);
return () => clearInterval(t);
});
// One representative of every kind, staged on a 3x3 (sectors on a 5x5).
const mid = { x: 1, y: 1 };
const SAMPLES: BoardFx[] = ([
{ kind: "fireball", from: { x: 0, y: 1 }, to: { x: 2, y: 1 } },
{ kind: "waterbolt", from: { x: 0, y: 1 }, to: { x: 2, y: 1 } },
{ kind: "bolt", from: { x: 0, y: 0 }, to: { x: 2, y: 2 } },
{ kind: "burst", at: mid },
{ kind: "splash", at: mid },
{ kind: "shimmer", at: mid },
{ kind: "shield", at: mid },
{ kind: "sparkle", at: mid },
{ kind: "whiff", at: mid },
{ kind: "hit", at: mid },
{ kind: "pow", at: mid },
{ kind: "claw", at: mid },
{ kind: "absorb", at: mid },
{ kind: "portal", cell: mid, side: "E" },
{ kind: "portal-cell", at: mid },
{ kind: "streak", from: { x: 0, y: 2 }, to: { x: 2, y: 0 } },
{ kind: "soul", at: { x: 1, y: 2 } },
{ kind: "fireworks", at: mid },
{ kind: "chaos-swirl", at: mid },
{ kind: "pit-fall", at: mid },
{ kind: "ooze-slip", at: mid },
{ kind: "tacks-ow", at: mid },
{ kind: "thorn-snap", at: mid },
{ kind: "slime-stuck", at: mid },
{ kind: "dust-puff", at: mid },
{ kind: "edge-dust", cell: mid, side: "S" },
{ kind: "sector-spin", origin: { x: 0, y: 0 }, clockwise: true },
{ kind: "sector-slide", from: { x: 0, y: 0 }, to: { x: 1, y: 0 } },
] as unknown as BoardFx[]).map((f, i) => ({ ...f, id: i + 1 }));
const isSector = (k: string) => k.startsWith("sector-");
</script>
<div class="gallery">
<h1>The Flourish Workshop</h1>
<p class="sub">
Every effect replays each beat. Edit a file in <code>src/fx-sprites/</code>
and it hot-reloads here.
</p>
<div class="grid">
{#each SAMPLES as fx (fx.kind)}
{@const Sprite = FX_SPRITES[fx.kind]}
<figure>
<svg viewBox={isSector(fx.kind) ? "-4 -4 296 296" : "-4 -4 152 152"}>
{#each Array.from({ length: isSector(fx.kind) ? 6 : 3 }, (_, cy) => cy) as cy (cy)}
{#each Array.from({ length: isSector(fx.kind) ? 6 : 3 }, (_, cx) => cx) as cx (cx)}
<rect x={cx * 48} y={cy * 48} width="48" height="48" class="cell" />
{/each}
{/each}
{#key tick}
<g class="fx-layer"><Sprite fx={fx as never} /></g>
{/key}
</svg>
<figcaption>{fx.kind}</figcaption>
</figure>
{/each}
</div>
</div>
<style>
.gallery {
min-height: 100vh;
background: #171a20;
color: #d8d2c0;
font-family: "Archivo Narrow", system-ui, sans-serif;
padding: 1.5rem;
}
h1 {
font-family: "Oswald", sans-serif;
text-transform: uppercase;
letter-spacing: 0.14em;
font-size: 1.2rem;
color: #e9e1cb;
margin: 0 0 0.2rem;
}
.sub { color: #8d8672; margin: 0 0 1.2rem; }
.sub code { color: #c9a72a; }
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 1rem;
}
figure {
margin: 0;
background: #efe8d4;
border-radius: 4px;
padding: 0.5rem;
}
svg { width: 100%; display: block; }
.cell {
fill: #efe8d4;
stroke: rgba(95, 74, 51, 0.25);
stroke-width: 1;
}
.fx-layer { pointer-events: none; }
figcaption {
font-family: "Courier Prime", monospace;
font-size: 0.75rem;
color: #43331f;
text-align: center;
padding-top: 0.3rem;
}
</style>