All 45 tokens in both arts — the photographed physical set beside the hand-drawn contingency — grouped (wizards, treasures, monsters, terrain, objects & markers), each at board size and close-up. The drawn column renders through TokenArt, so it previews exactly as the board inlines it. Cross-linked with the flourish workshop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
193 lines
6.6 KiB
Svelte
193 lines
6.6 KiB
Svelte
<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.
|
|
import type { Component } from "svelte";
|
|
import { FX_SPRITES } from "./fx-sprites";
|
|
import type { BoardFx, FxShape } from "./fx";
|
|
import FirewallEdge from "./FirewallEdge.svelte";
|
|
import IllusionShimmer from "./IllusionShimmer.svelte";
|
|
import SightTraceOverlay from "./SightTraceOverlay.svelte";
|
|
|
|
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", a: { x: 24, y: 72 }, b: { x: 120, y: 72 } },
|
|
{ kind: "waterbolt", a: { x: 24, y: 72 }, b: { x: 120, y: 72 } },
|
|
{ kind: "bolt", a: { x: 24, y: 24 }, b: { x: 120, y: 120 } },
|
|
{ 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", a: { x: 24, y: 120 }, b: { x: 120, y: 24 } },
|
|
{ kind: "soul", at: { x: 1, y: 2 } },
|
|
{ kind: "fireworks", at: mid },
|
|
{ kind: "chaos-swirl", at: mid },
|
|
{ kind: "die-drop", at: { x: 1, y: 1 }, aim: { x: 0, y: 2 } },
|
|
{ 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 FxShape[]).map((f, i): BoardFx => ({ ...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.
|
|
<a class="cross" href="/?tokens">→ the token workshop</a>
|
|
</p>
|
|
<div class="grid">
|
|
{#each SAMPLES as fx (fx.kind)}
|
|
{@const Sprite = FX_SPRITES[fx.kind] as Component<{ fx: BoardFx }>}
|
|
<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} /></g>
|
|
{/key}
|
|
</svg>
|
|
<figcaption>{fx.kind}</figcaption>
|
|
</figure>
|
|
{/each}
|
|
</div>
|
|
|
|
<h1 class="furniture-head">The standing fires</h1>
|
|
<p class="sub">
|
|
Board furniture that burns as long as its spell holds — these loop by
|
|
nature, no beat needed. Same components the live board mounts.
|
|
</p>
|
|
<div class="grid">
|
|
<figure>
|
|
<svg viewBox="-4 -4 152 152">
|
|
{#each [0, 1, 2] as cy (cy)}{#each [0, 1, 2] as cx (cx)}
|
|
<rect x={cx * 48} y={cy * 48} width="48" height="48" class="cell" />
|
|
{/each}{/each}
|
|
<FirewallEdge x={1} y={0} kind="H" />
|
|
</svg>
|
|
<figcaption>wall of fire (H)</figcaption>
|
|
</figure>
|
|
<figure>
|
|
<svg viewBox="-4 -4 152 152">
|
|
{#each [0, 1, 2] as cy (cy)}{#each [0, 1, 2] as cx (cx)}
|
|
<rect x={cx * 48} y={cy * 48} width="48" height="48" class="cell" />
|
|
{/each}{/each}
|
|
<FirewallEdge x={0} y={1} kind="V" />
|
|
</svg>
|
|
<figcaption>wall of fire (V)</figcaption>
|
|
</figure>
|
|
<figure>
|
|
<svg viewBox="-4 -4 152 152">
|
|
{#each [0, 1, 2] as cy (cy)}{#each [0, 1, 2] as cx (cx)}
|
|
<rect x={cx * 48} y={cy * 48} width="48" height="48" class="cell" />
|
|
{/each}{/each}
|
|
<rect x={93.5} y={44.5} width={7} height={55} class="demo-wall" />
|
|
<IllusionShimmer x={1} y={1} kind="V" />
|
|
</svg>
|
|
<figcaption>illusion shimmer</figcaption>
|
|
</figure>
|
|
<figure>
|
|
<svg viewBox="-4 -4 152 152">
|
|
{#each [0, 1, 2] as cy (cy)}{#each [0, 1, 2] as cx (cx)}
|
|
<rect x={cx * 48} y={cy * 48} width="48" height="48" class="cell" />
|
|
{/each}{/each}
|
|
<SightTraceOverlay from={{ x: 0, y: 2 }} to={{ x: 2, y: 0 }} trace={{ kind: "direct" }} />
|
|
</svg>
|
|
<figcaption>sight trace (direct)</figcaption>
|
|
</figure>
|
|
<figure>
|
|
<svg viewBox="-4 -4 152 152">
|
|
{#each [0, 1, 2] as cy (cy)}{#each [0, 1, 2] as cx (cx)}
|
|
<rect x={cx * 48} y={cy * 48} width="48" height="48" class="cell" />
|
|
{/each}{/each}
|
|
<SightTraceOverlay from={{ x: 0, y: 2 }} to={{ x: 2, y: 1 }}
|
|
trace={{ kind: "warp", mouthA: { cell: { x: 1, y: 0 }, side: "N" },
|
|
mouthB: { cell: { x: 1, y: 2 }, side: "S" },
|
|
entry: { x: 1.5, y: 0 }, exit: { x: 1.5, y: 3 } }} />
|
|
</svg>
|
|
<figcaption>sight trace (through a warp)</figcaption>
|
|
</figure>
|
|
</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; }
|
|
.furniture-head {
|
|
font-family: "Oswald", sans-serif;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.14em;
|
|
font-size: 1.2rem;
|
|
color: #e9e1cb;
|
|
margin: 1.6rem 0 0.2rem;
|
|
}
|
|
.demo-wall { fill: #4d4438; }
|
|
.sub code { color: #c9a72a; }
|
|
.sub .cross { 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>
|