From e947e8e1274e63834495106bc22e614280b20786 Mon Sep 17 00:00:00 2001 From: Aaron Wood Date: Fri, 10 Apr 2026 19:25:03 -0400 Subject: [PATCH] feat: add ParticleOverlay component for tsParticles effects --- client/src/components/ParticleOverlay.tsx | 75 +++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 client/src/components/ParticleOverlay.tsx diff --git a/client/src/components/ParticleOverlay.tsx b/client/src/components/ParticleOverlay.tsx new file mode 100644 index 0000000..a7f7109 --- /dev/null +++ b/client/src/components/ParticleOverlay.tsx @@ -0,0 +1,75 @@ +import { useEffect, useState } from "react"; +import Particles, { initParticlesEngine } from "@tsparticles/react"; +import { loadSlim } from "@tsparticles/slim"; +import type { AtmosphereState } from "../lib/atmosphereTypes"; +import { getFireConfig, getRainConfig, getEmbersConfig } from "../lib/particleConfigs"; + +// Module-level singleton so the engine is only initialised once +let enginePromise: Promise | null = null; +function ensureEngine(): Promise { + if (!enginePromise) { + enginePromise = initParticlesEngine(async (engine) => { + await loadSlim(engine); + }); + } + return enginePromise; +} + +const overlayStyle: React.CSSProperties = { + position: "fixed", + inset: 0, + zIndex: 9997, + pointerEvents: "none", +}; + +const instanceStyle: React.CSSProperties = { + position: "absolute", + inset: 0, +}; + +interface ParticleOverlayProps { + atmosphere: AtmosphereState; +} + +export default function ParticleOverlay({ atmosphere }: ParticleOverlayProps) { + const [ready, setReady] = useState(false); + + useEffect(() => { + ensureEngine().then(() => setReady(true)); + }, []); + + if (!ready) return null; + + const { fire, rain, embers } = atmosphere; + const anyActive = fire.active || rain.active || embers.active; + if (!anyActive) return null; + + return ( +
+ {fire.active && ( + + )} + {rain.active && ( + + )} + {embers.active && ( + + )} +
+ ); +}