jellyfin-tools/jellyfin-year-review/web/src/lib/Confetti.svelte
jellyfin-year-review/web/src/lib/Confetti.svelteRaw
1<script>
2 const colors = ['#c5f76b', '#a78bfa', '#ff7869', '#ffd166', '#65d6ed', '#ff86c8'];
3 // Spread a short shower across the viewport, with varied timing and tumbling.
4 const pieces = Array.from({ length: 90 }, (_, i) => ({
5 left: (i * 37) % 100,
6 delay: ((i * 13) % 120) / 100,
7 duration: 3 + ((i * 17) % 140) / 100,
8 drift: ((i * 29) % 240) - 120,
9 rotation: (i % 2 ? 1 : -1) * (540 + (i * 31) % 720),
10 color: colors[i % colors.length]
11 }));
12</script>
13
14<div class="confetti" aria-hidden="true">
15 {#each pieces as piece, i}
16 <span style="left: {piece.left}%; background: {piece.color}; --delay: {piece.delay}s; --duration: {piece.duration}s; --drift: {piece.drift}px; --rotation: {piece.rotation}deg; border-radius: {i % 3 === 0 ? '50%' : '1px'};"></span>
17 {/each}
18</div>
19
20<style>
21 .confetti { position: fixed; inset: 0; z-index: 100; overflow: hidden; pointer-events: none; }
22 span { position: absolute; top: -24px; width: 8px; height: 14px; animation: fall var(--duration) var(--delay) linear both; }
23 @keyframes fall {
24 from { transform: translate3d(0, 0, 0) rotate(0deg); }
25 to { transform: translate3d(var(--drift), calc(100dvh + 48px), 0) rotateZ(var(--rotation)) rotateY(720deg); }
26 }
27 @media (prefers-reduced-motion: reduce) {
28 .confetti { display: none; }
29 }
30</style>