feat: add snow atmosphere effect

tsParticles snow — 60–300 flakes depending on intensity, size variation
for depth, gentle drift via random speed + curved paths. Appears in
atmosphere panel between Rain and Embers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Aaron Wood 2026-04-11 23:30:39 -04:00
parent 97954de110
commit a923e1b226
4 changed files with 45 additions and 2 deletions

View file

@ -8,6 +8,7 @@ const EFFECTS: { key: EffectKey; emoji: string; label: string }[] = [
{ key: "fog", emoji: "🌫", label: "Fog" }, { key: "fog", emoji: "🌫", label: "Fog" },
{ key: "fire", emoji: "🔥", label: "Fire" }, { key: "fire", emoji: "🔥", label: "Fire" },
{ key: "rain", emoji: "🌧", label: "Rain" }, { key: "rain", emoji: "🌧", label: "Rain" },
{ key: "snow", emoji: "❄️", label: "Snow" },
{ key: "embers", emoji: "✨", label: "Embers" }, { key: "embers", emoji: "✨", label: "Embers" },
]; ];
@ -26,6 +27,7 @@ export default function AtmospherePanel({
fog: atmosphere.fog.intensity, fog: atmosphere.fog.intensity,
fire: atmosphere.fire.intensity, fire: atmosphere.fire.intensity,
rain: atmosphere.rain.intensity, rain: atmosphere.rain.intensity,
snow: atmosphere.snow.intensity,
embers: atmosphere.embers.intensity, embers: atmosphere.embers.intensity,
}); });
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
@ -48,6 +50,7 @@ export default function AtmospherePanel({
fog: atmosphere.fog.intensity, fog: atmosphere.fog.intensity,
fire: atmosphere.fire.intensity, fire: atmosphere.fire.intensity,
rain: atmosphere.rain.intensity, rain: atmosphere.rain.intensity,
snow: atmosphere.snow.intensity,
embers: atmosphere.embers.intensity, embers: atmosphere.embers.intensity,
}); });
}, [atmosphere]); }, [atmosphere]);

View file

@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
import Particles, { initParticlesEngine } from "@tsparticles/react"; import Particles, { initParticlesEngine } from "@tsparticles/react";
import { loadSlim } from "@tsparticles/slim"; import { loadSlim } from "@tsparticles/slim";
import type { AtmosphereState } from "../lib/atmosphereTypes"; import type { AtmosphereState } from "../lib/atmosphereTypes";
import { getRainConfig, getEmbersConfig } from "../lib/particleConfigs"; import { getRainConfig, getSnowConfig, getEmbersConfig } from "../lib/particleConfigs";
// Module-level singleton so the engine is only initialised once // Module-level singleton so the engine is only initialised once
let enginePromise: Promise<void> | null = null; let enginePromise: Promise<void> | null = null;
@ -28,7 +28,7 @@ export default function ParticleOverlay({ atmosphere }: ParticleOverlayProps) {
if (!ready) return null; if (!ready) return null;
const { rain, embers } = atmosphere; const { rain, snow, embers } = atmosphere;
return ( return (
<> <>
@ -39,6 +39,13 @@ export default function ParticleOverlay({ atmosphere }: ParticleOverlayProps) {
options={getRainConfig(rain.intensity)} options={getRainConfig(rain.intensity)}
/> />
)} )}
{snow.active && (
<Particles
key={`snow-${snow.intensity}`}
id="particles-snow"
options={getSnowConfig(snow.intensity)}
/>
)}
{embers.active && ( {embers.active && (
<Particles <Particles
key={`embers-${embers.intensity}`} key={`embers-${embers.intensity}`}

View file

@ -7,6 +7,7 @@ export interface AtmosphereState {
fog: EffectState; fog: EffectState;
fire: EffectState; fire: EffectState;
rain: EffectState; rain: EffectState;
snow: EffectState;
embers: EffectState; embers: EffectState;
} }
@ -14,5 +15,6 @@ export const defaultAtmosphere: AtmosphereState = {
fog: { active: false, intensity: 50 }, fog: { active: false, intensity: 50 },
fire: { active: false, intensity: 50 }, fire: { active: false, intensity: 50 },
rain: { active: false, intensity: 50 }, rain: { active: false, intensity: 50 },
snow: { active: false, intensity: 50 },
embers: { active: false, intensity: 50 }, embers: { active: false, intensity: 50 },
}; };

View file

@ -29,6 +29,37 @@ export function getRainConfig(intensity: number): ISourceOptions {
}; };
} }
/**
* Snow intensity 0100 count 60300, speed 13.5
* Gentle falling flakes with wobble drift. Size varies for depth illusion.
*/
export function getSnowConfig(intensity: number): ISourceOptions {
const count = Math.round(60 + (intensity / 100) * 240);
const speed = 1 + (intensity / 100) * 2.5;
return {
fullScreen: { enable: true, zIndex: 9997 },
particles: {
number: { value: count, density: { enable: false } },
color: { value: ["#ffffff", "#ddeeff", "#eef6ff"] },
shape: { type: "circle" },
opacity: {
value: { min: 0.25, max: 0.75 },
animation: { enable: true, speed: 0.3, sync: false, startValue: "random" },
},
size: { value: { min: 1, max: 5 } },
move: {
enable: true,
speed: speed,
direction: "bottom",
random: true,
straight: false,
outModes: { default: "out" },
},
},
interactivity: { events: { onClick: { enable: false }, onHover: { enable: false } } },
};
}
/** /**
* Embers intensity 0100 count 50150, speed 0.52 * Embers intensity 0100 count 50150, speed 0.52
* Smaller, more numerous slow-drifting sparks. Flickering opacity. * Smaller, more numerous slow-drifting sparks. Flickering opacity.