The fireball sets the bar, and the rest of the catalog rises to it

Eric's fireball lands as the house style — layered construction,
gradient core, glow halo, a directional tail riding rotate="auto" —
with its defs scoped per instance via $props.id(), so simultaneous
fireballs cannot steal each other's gradients. The rest of the
catalog is rebuilt in its image: the waterbolt becomes its cold
sibling (spray tail, no turbulence — one heavy filter stack is
enough); lightning grows a glowing core and branching forks; the
burst blooms through a fire gradient with a shockwave and flying
embers; the splash throws a crown of droplets; the shield raises a
gradient dome with a rim strike; the pow star flashes white and
radiates speed dashes; claws leave tapered gashes; the absorbed card
spirals in along a pull ring; the soul is a proper little ghost with
eyes, swaying as it rises; fireworks ride colored trails; Chaos
catches card slips in its vortex; the pit opens a gradient hole that
swallows a faller; ooze splats and flings blobs; the thornbush sheds
leaves; slime closes over and pops bubbles; masonry dust goes soft
with falling grit. Whiff, tacks, portal, and the sector ghosts keep
their simpler forms on purpose — restraint is part of the style.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-17 10:16:01 -04:00
co-authored by Claude Fable 5
parent fe0318b99b
commit 308f1b6ff9
19 changed files with 688 additions and 167 deletions
+38 -9
View File
@@ -1,26 +1,55 @@
<script lang="ts">
import type { FxOf } from "../fx";
import { center } from "./geom";
let { fx }: { fx: FxOf<"fireworks"> } = $props();
const c = $derived(center(fx.at));
const COLORS = ["#e74c3c", "#f1c40f", "#3b8dd6", "#7ac47e"];
const RAYS = [0, 45, 90, 135, 180, 225, 270, 315];
</script>
<g class="fireworks" style={`transform-origin: ${c.x}px ${c.y}px`}>
{#each [0, 45, 90, 135, 180, 225, 270, 315] as deg (deg)}
<g class="fireworks">
{#each RAYS as deg, i (deg)}
{@const dx = Math.cos((deg * Math.PI) / 180)}
{@const dy = Math.sin((deg * Math.PI) / 180)}
<line
x1={c.x + 4 * dx} y1={c.y + 4 * dy}
x2={c.x + 20 * dx} y2={c.y + 20 * dy}
stroke={COLORS[i % 4]}
class="trail"
style={`transform-origin: ${c.x}px ${c.y}px`}
/>
<circle
cx={c.x + 18 * Math.cos((deg * Math.PI) / 180)}
cy={c.y + 18 * Math.sin((deg * Math.PI) / 180)}
r="3" fill={COLORS[(deg / 45) % 4]}
cx={c.x + 21 * dx} cy={c.y + 21 * dy} r="2.6"
fill={COLORS[i % 4]}
class="spark"
style={`transform-origin: ${c.x}px ${c.y}px`}
/>
{/each}
<circle cx={c.x} cy={c.y} r="3.5" class="heart" />
</g>
<style>
.fireworks { animation: boom 1.1s ease-out forwards; }
@keyframes boom {
0% { opacity: 0; transform: scale(0.1); }
.trail {
stroke-width: 2;
stroke-linecap: round;
animation: trail-out 0.9s ease-out forwards;
}
.spark { animation: spark-out 1.05s ease-out forwards; }
.heart { fill: #fffdf0; animation: heart-pop 0.4s ease-out forwards; }
@keyframes trail-out {
0% { opacity: 0; transform: scale(0.15); }
25% { opacity: 1; }
100% { opacity: 0; transform: scale(2.4) rotate(30deg); }
100% { opacity: 0; transform: scale(1.5); }
}
@keyframes spark-out {
0% { opacity: 0; transform: scale(0.15); }
30% { opacity: 1; }
80% { opacity: 0.9; transform: scale(1.55); }
100% { opacity: 0; transform: scale(1.6) translateY(4px); }
}
@keyframes heart-pop {
0% { opacity: 1; transform: scale(0.5); }
100% { opacity: 0; transform: scale(3); }
}
</style>