// Ported from bmo-preview (https://github.com/PageZ948/bmo-preview). Visuals kept 1:1. /** * WeatherEffects — SVG group that renders animated weather particles. * * Supports: rain, snow, falling-leaves, pollen. * Particle count scales with `intensity` (0-1). * Positions are deterministic (index-based) — no Math.random(). * * All animations must be defined in index.css as garden-rain-fall, * garden-snow-fall, garden-leaf-fall, and garden-pollen-drift. */ import { memo } from "react"; interface WeatherEffectsProps { weather: string; intensity: number; sceneWidth: number; sceneHeight: number; } // ---- Deterministic helpers ---- /** Returns a value in [0, range) based on the index using a simple hash. */ function det(i: number, range: number, seed = 37): number { return ((i * seed + i * i * 13) % 1000) / 1000 * range; } function detRange(i: number, min: number, max: number, seed = 37): number { return min + det(i, max - min, seed); } // ---- Rain ---- function RainParticles({ count, sceneWidth, sceneHeight, }: { count: number; sceneWidth: number; sceneHeight: number; }) { return ( <> {Array.from({ length: count }, (_, i) => { const x = detRange(i, 0, sceneWidth, 37); const length = detRange(i, 4, 8, 53); const duration = detRange(i, 1, 2, 71); const delay = detRange(i, 0, 2, 89); const sw = detRange(i, 0.5, 1, 43); return ( ); })} ); } // ---- Snow ---- function SnowParticles({ count, sceneWidth, }: { count: number; sceneWidth: number; }) { return ( <> {Array.from({ length: count }, (_, i) => { const cx = detRange(i, 0, sceneWidth, 41); const cy = detRange(i, 0, 20, 59); const r = detRange(i, 0.5, 1.5, 67); const opacity = detRange(i, 0.3, 0.6, 79); const duration = detRange(i, 4, 8, 97); const delay = detRange(i, 0, 5, 113); return ( ); })} ); } // ---- Falling leaves ---- /** A tiny leaf silhouette path centered at 0,0 (~4px across). */ function leafPath(): string { return "M0,-2 C1,-1 2,0 1,2 C0,3 -1,2 0,0 C-1,-1 -2,0 0,-2Z"; } const LEAF_COLORS = ["#f59e0b", "#ea580c", "#dc2626", "#92400e", "#d97706"]; function FallingLeaves({ count, sceneWidth, }: { count: number; sceneWidth: number; }) { return ( <> {Array.from({ length: count }, (_, i) => { const x = detRange(i, 0, sceneWidth, 43); const y = detRange(i, -20, 0, 61); const color = LEAF_COLORS[i % LEAF_COLORS.length]; const duration = detRange(i, 5, 10, 83); const delay = detRange(i, 0, 6, 101); const scale = detRange(i, 0.8, 1.5, 107); return ( ); })} ); } // ---- Pollen ---- const POLLEN_COLORS = ["#fde047", "#a3e635", "#bef264", "#fef08a"]; function PollenParticles({ count, sceneWidth, sceneHeight, }: { count: number; sceneWidth: number; sceneHeight: number; }) { return ( <> {Array.from({ length: count }, (_, i) => { const cx = detRange(i, 0, sceneWidth, 47); const cy = detRange(i, sceneHeight * 0.3, sceneHeight * 0.8, 71); const r = detRange(i, 0.3, 0.8, 89); const color = POLLEN_COLORS[i % POLLEN_COLORS.length]; const duration = detRange(i, 6, 12, 97); const delay = detRange(i, 0, 8, 113); return ( ); })} ); } // ---- Main component ---- export const WeatherEffects = memo(function WeatherEffects({ weather, intensity, sceneWidth, sceneHeight, }: WeatherEffectsProps) { if (weather === "none" || intensity <= 0) return null; const clampedIntensity = Math.max(0, Math.min(1, intensity)); if (weather === "rain") { const count = Math.round(20 + clampedIntensity * 40); return ( ); } if (weather === "snow") { const count = Math.round(15 + clampedIntensity * 25); return ( ); } if (weather === "falling-leaves") { const count = Math.round(8 + clampedIntensity * 12); return ( ); } if (weather === "pollen") { const count = Math.round(10 + clampedIntensity * 15); return ( ); } return null; });