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>
68 lines
2.2 KiB
Svelte
68 lines
2.2 KiB
Svelte
<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"];
|
|
// 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 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 + ray.len * dx} y2={c.y + ray.len * dy}
|
|
stroke={COLORS[i % 4]}
|
|
class={`trail ${ray.cls}`}
|
|
style={`transform-origin: ${c.x}px ${c.y}px`}
|
|
/>
|
|
<circle
|
|
cx={c.x + (ray.len + 1) * dx} cy={c.y + (ray.len + 1) * dy} r={ray.r}
|
|
fill={COLORS[i % 4]}
|
|
class={`spark ${ray.cls}`}
|
|
style={`transform-origin: ${c.x}px ${c.y}px`}
|
|
/>
|
|
{/each}
|
|
<circle cx={c.x} cy={c.y} r="3.5" class="heart" />
|
|
</g>
|
|
|
|
<style>
|
|
.trail {
|
|
stroke-width: 2;
|
|
stroke-linecap: round;
|
|
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); }
|
|
25% { opacity: 1; }
|
|
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>
|