The optional final polish: five refinements

The bolt deepens to saturated gold over a dark amber glow; the splash
throws a wider crown of five droplets over a broader pool; the
fireworks lose their protractor — uneven angles, lengths, sizes, and
staggered timing, as a real burst has; the portal-cell's void gains
turning swirl arms so the dark center reads as motion, not blur; the
streak narrows and softens so its peak frame no longer resembles a
drawn white line.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-17 10:37:57 -04:00
co-authored by Claude Fable 5
parent e713d69582
commit c6d73550df
5 changed files with 56 additions and 22 deletions
+5 -4
View File
@@ -45,10 +45,10 @@
<style>
.bolt .wide {
fill: none;
stroke: #ffe94d;
stroke-width: 5;
stroke: #e6a812;
stroke-width: 5.5;
stroke-linejoin: round;
opacity: 0.55;
opacity: 0.75;
}
.bolt .fork {
fill: none;
@@ -59,9 +59,10 @@
.bolt .core {
fill: none;
stroke: #fffdf0;
stroke-width: 1.8;
stroke-width: 2;
stroke-linejoin: round;
}
.bolt .fork { stroke: #f5d54a; }
.bolt { animation: bolt-flicker 0.5s steps(2, jump-none) forwards; }
@keyframes bolt-flicker {
0% { opacity: 0; } 15% { opacity: 1; } 40% { opacity: 0.3; }
+20 -8
View File
@@ -5,24 +5,34 @@
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];
// Deliberately uneven: a real burst has no protractor.
const RAYS = [
{ deg: 8, len: 22, r: 2.8, cls: "" },
{ deg: 52, len: 17, r: 2.2, cls: "late" },
{ deg: 88, len: 24, r: 3, cls: "" },
{ deg: 141, len: 16, r: 2, cls: "later" },
{ deg: 176, len: 21, r: 2.6, cls: "late" },
{ deg: 224, len: 18, r: 2.3, cls: "" },
{ deg: 267, len: 23, r: 2.8, cls: "later" },
{ deg: 309, len: 15, r: 2, cls: "late" },
];
</script>
<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)}
{#each RAYS as ray, i (ray.deg)}
{@const dx = Math.cos((ray.deg * Math.PI) / 180)}
{@const dy = Math.sin((ray.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}
x2={c.x + ray.len * dx} y2={c.y + ray.len * dy}
stroke={COLORS[i % 4]}
class="trail"
class={`trail ${ray.cls}`}
style={`transform-origin: ${c.x}px ${c.y}px`}
/>
<circle
cx={c.x + 21 * dx} cy={c.y + 21 * dy} r="2.6"
cx={c.x + (ray.len + 1) * dx} cy={c.y + (ray.len + 1) * dy} r={ray.r}
fill={COLORS[i % 4]}
class="spark"
class={`spark ${ray.cls}`}
style={`transform-origin: ${c.x}px ${c.y}px`}
/>
{/each}
@@ -36,6 +46,8 @@
animation: trail-out 0.9s ease-out forwards;
}
.spark { animation: spark-out 1.05s ease-out forwards; }
.trail.late, .spark.late { animation-delay: 0.07s; }
.trail.later, .spark.later { animation-delay: 0.14s; }
.heart { fill: #fffdf0; animation: heart-pop 0.4s ease-out forwards; }
@keyframes trail-out {
0% { opacity: 0; transform: scale(0.15); }
+20
View File
@@ -24,6 +24,13 @@
width={CELL - 10} height={CELL - 10} rx="8"
fill={`url(#portal-void-${uid})`} class="void"
style={`transform-origin: ${fx.at.x * CELL + CELL / 2}px ${fx.at.y * CELL + CELL / 2}px`} />
{@const vx = fx.at.x * CELL + CELL / 2}
{@const vy = fx.at.y * CELL + CELL / 2}
<g class="swirl" style={`transform-origin: ${vx}px ${vy}px`}>
<path d={`M ${vx + 12} ${vy} A 12 12 0 0 1 ${vx - 6} ${vy + 10}`} class="arm" />
<path d={`M ${vx - 12} ${vy} A 12 12 0 0 1 ${vx + 6} ${vy - 10}`} class="arm" />
<path d={`M ${vx} ${vy + 7} A 7 7 0 0 1 ${vx - 6} ${vy - 4}`} class="arm faint" />
</g>
<rect x={fx.at.x * CELL + 3} y={fx.at.y * CELL + 3}
width={CELL - 6} height={CELL - 6} rx="6" class="veil outer"
style={`transform-origin: ${fx.at.x * CELL + CELL / 2}px ${fx.at.y * CELL + CELL / 2}px`} />
@@ -68,6 +75,19 @@
filter: drop-shadow(0 0 4px rgba(155, 227, 224, 0.8));
}
.void { animation: void-open 0.9s ease-out forwards; }
.arm {
fill: none;
stroke: rgba(155, 227, 224, 0.7);
stroke-width: 1.8;
stroke-linecap: round;
}
.arm.faint { stroke: rgba(180, 138, 224, 0.55); }
.swirl { animation: swirl-turn 0.9s ease-in forwards; }
@keyframes swirl-turn {
0% { opacity: 0; transform: rotate(0deg) scale(1.1); }
25% { opacity: 1; }
100% { opacity: 0; transform: rotate(160deg) scale(0.4); }
}
.veil.outer { stroke-width: 2.5; animation: veil-in 0.9s ease-in forwards; }
.veil.inner {
stroke: rgba(180, 138, 224, 0.8);
+8 -7
View File
@@ -15,14 +15,15 @@
</radialGradient>
</defs>
<ellipse cx={c.x} cy={c.y + 4} rx="12" ry="5" fill={`url(#splash-pool-${uid})`} class="pool" />
<circle cx={c.x} cy={c.y} r="5" class="ring" />
<ellipse cx={c.x} cy={c.y + 5} rx="16" ry="6.5" fill={`url(#splash-pool-${uid})`} class="pool" />
<circle cx={c.x} cy={c.y} r="7" class="ring" />
<!-- The crown: droplets thrown upward, falling back -->
<g class="crown">
<circle cx={c.x - 10} cy={c.y - 6} r="2.5" class="drop" />
<circle cx={c.x - 3} cy={c.y - 12} r="2" class="drop late" />
<circle cx={c.x + 5} cy={c.y - 10} r="2.2" class="drop" />
<circle cx={c.x + 11} cy={c.y - 5} r="1.7" class="drop later" />
<circle cx={c.x - 14} cy={c.y - 6} r="2.8" class="drop" />
<circle cx={c.x - 7} cy={c.y - 13} r="2.3" class="drop late" />
<circle cx={c.x + 1} cy={c.y - 16} r="2.5" class="drop" />
<circle cx={c.x + 8} cy={c.y - 12} r="2.2" class="drop later" />
<circle cx={c.x + 14} cy={c.y - 5} r="1.9" class="drop late" />
</g>
<style>
@@ -36,7 +37,7 @@
transform-origin: center;
fill: none;
stroke: #7cc0ee;
stroke-width: 2.5;
stroke-width: 3.2;
animation: ring-out 0.55s ease-out forwards;
}
.drop { fill: #7cc0ee; animation: drop-arc 0.6s ease-in forwards; }
+3 -3
View File
@@ -10,7 +10,7 @@
<path {d} pathLength="100" class="trail wide" />
<path {d} pathLength="100" class="trail bright" />
<circle r="4.5" class="head" cx="0" cy="0">
<circle r="4" class="head" cx="0" cy="0">
<animateMotion dur="0.38s" fill="freeze" path={d} calcMode="spline"
keySplines="0.3 0 0.4 1" keyTimes="0;1" keyPoints="0;1" />
</circle>
@@ -22,8 +22,8 @@
stroke-dasharray: 100 100;
animation: draw-collapse 0.68s ease-out forwards;
}
.trail.wide { stroke: rgba(233, 225, 203, 0.4); stroke-width: 7; }
.trail.bright { stroke: rgba(255, 253, 240, 0.9); stroke-width: 3; }
.trail.wide { stroke: rgba(233, 225, 203, 0.3); stroke-width: 5; }
.trail.bright { stroke: rgba(255, 253, 240, 0.75); stroke-width: 2; }
.head {
fill: #fffdf0;
animation: head-life 0.6s ease-out forwards;