Skip to content

Commit 25e8ebf

Browse files
authored
Merge pull request #84 from hackclub/staging
Snow
2 parents 769d3e6 + 41fdc29 commit 25e8ebf

File tree

2 files changed

+147
-1
lines changed

2 files changed

+147
-1
lines changed

src/lib/components/Snowfall.svelte

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<script lang="ts">
2+
import { onDestroy, onMount } from 'svelte';
3+
4+
let canvas: HTMLCanvasElement;
5+
let ctx: CanvasRenderingContext2D | null = null;
6+
let width = 0;
7+
let height = 0;
8+
let dpr = 1;
9+
let raf = 0;
10+
11+
interface Flake {
12+
x: number;
13+
y: number;
14+
r: number;
15+
vx: number;
16+
vy: number;
17+
phase: number;
18+
opacity: number;
19+
}
20+
21+
let flakes: Flake[] = [];
22+
23+
function makeFlake(): Flake {
24+
const r = Math.random() * 2.2 + 0.8; // 0.8 - 3.0 px
25+
const speed = 0.4 + r * 0.35; // tie speed to size
26+
return {
27+
x: Math.random() * width,
28+
y: -10 - Math.random() * height,
29+
r,
30+
vx: (Math.random() - 0.5) * 0.4,
31+
vy: speed,
32+
phase: Math.random() * Math.PI * 2,
33+
opacity: 0.6 + Math.random() * 0.4
34+
};
35+
}
36+
37+
function setCanvasSize() {
38+
width = window.innerWidth;
39+
height = window.innerHeight;
40+
canvas.width = Math.floor(width * dpr);
41+
canvas.height = Math.floor(height * dpr);
42+
canvas.style.width = width + 'px';
43+
canvas.style.height = height + 'px';
44+
ctx && ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
45+
}
46+
47+
function populateFlakes(target: number) {
48+
flakes.length = 0;
49+
for (let i = 0; i < target; i++) {
50+
const f = makeFlake();
51+
f.y = Math.random() * height;
52+
flakes.push(f);
53+
}
54+
}
55+
56+
function draw() {
57+
if (!ctx) return;
58+
ctx.clearRect(0, 0, width, height);
59+
ctx.fillStyle = '#fff';
60+
for (const f of flakes) {
61+
f.phase += 0.01 + f.r * 0.002;
62+
f.x += f.vx + Math.sin(f.phase) * 0.3;
63+
f.y += f.vy;
64+
65+
if (f.y - f.r > height) {
66+
f.x = Math.random() * width;
67+
f.y = -f.r - Math.random() * 40;
68+
f.vx = (Math.random() - 0.5) * 0.4;
69+
f.phase = Math.random() * Math.PI * 2;
70+
}
71+
72+
ctx.globalAlpha = f.opacity;
73+
ctx.beginPath();
74+
ctx.arc(f.x, f.y, f.r, 0, Math.PI * 2);
75+
ctx.fill();
76+
}
77+
ctx.globalAlpha = 1;
78+
}
79+
80+
function loop() {
81+
draw();
82+
raf = requestAnimationFrame(loop);
83+
}
84+
85+
function handleResize() {
86+
setCanvasSize();
87+
const density = Math.min(220, Math.max(60, Math.floor((width * height) / 15000)));
88+
populateFlakes(density);
89+
}
90+
91+
onMount(() => {
92+
if (typeof window === 'undefined') return;
93+
dpr = Math.min(2, window.devicePixelRatio || 1);
94+
ctx = canvas.getContext('2d');
95+
if (!ctx) return;
96+
setCanvasSize();
97+
handleResize();
98+
window.addEventListener('resize', handleResize, { passive: true });
99+
raf = requestAnimationFrame(loop);
100+
return () => {
101+
if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(raf);
102+
window.removeEventListener('resize', handleResize);
103+
};
104+
});
105+
106+
onDestroy(() => {
107+
if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(raf);
108+
if (typeof window !== 'undefined') window.removeEventListener('resize', handleResize);
109+
});
110+
</script>
111+
112+
<style>
113+
canvas {
114+
position: fixed;
115+
inset: 0;
116+
width: 100%;
117+
height: 100%;
118+
pointer-events: none;
119+
z-index: 0;
120+
filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.6));
121+
}
122+
@media (prefers-reduced-motion: reduce) {
123+
canvas { display: none; }
124+
}
125+
</style>
126+
127+
<canvas bind:this={canvas} aria-hidden="true"></canvas>

src/routes/+layout.svelte

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,26 @@
11
<script lang="ts">
22
import '../app.css';
3+
import Snowfall from '$lib/components/Snowfall.svelte';
34
45
let { children } = $props();
56
</script>
67

7-
{@render children?.()}
8+
<Snowfall />
9+
10+
<div class="app-content">
11+
{@render children?.()}
12+
</div>
13+
14+
<style>
15+
.app-content {
16+
position: relative;
17+
z-index: 1;
18+
min-height: 100dvh;
19+
}
20+
:global(html, body) {
21+
min-height: 100%;
22+
}
23+
:global(body) {
24+
position: relative;
25+
}
26+
</style>

0 commit comments

Comments
 (0)