// 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;
});