4.5 KiB
Particle Effects / Atmosphere Panel
Date: 2026-04-10
Status: Approved
Summary
Replace the single fog toggle button in the campaign header with an atmosphere panel that supports four effects: fog (CSS, existing), fire, rain, and embers (tsParticles). Effects stack independently. Each effect has an on/off toggle and an intensity slider (0–100).
Library
@tsparticles/react + @tsparticles/slim
- Slim engine includes all shapes and movers needed for fire, rain, embers
- Intensity maps directly onto particle config params (count, speed)
- FogOverlay stays as CSS — no library change for fog
Components
AtmospherePanel.tsx (new)
Button trigger in the campaign header, replacing the existing fog <button>. When one or more effects are active the button shows their icons (e.g. 🌫🔥). Clicking opens a popover panel anchored below the button. Clicking outside closes it.
Panel contains one row per effect (Fog, Fire, Rain, Embers):
- Label with emoji
- Toggle switch (on/off)
- Intensity slider (0–100), dimmed when effect is off
On any toggle or slider change, the component calls an onAtmosphereChange(newAtmosphere) callback. CampaignView owns the socket; its handler calls setAtmosphere(next) and socket.emit("atmosphere:update", { campaignId, ...next }). AtmospherePanel receives only atmosphere state and onAtmosphereChange as props — no direct socket access.
Fog intensity slider is rendered but has no mechanical effect in v1 (CSS fog doesn't have an intensity param). It will not be wired until the fog overlay supports it.
ParticleOverlay.tsx (new)
Full-screen fixed canvas, pointer-events: none, z-index 9997 (just below FogOverlay at 9998). Receives the full atmosphere state. For each of fire, rain, embers: if active === true, renders a tsParticles instance with a config generated from getXxxConfig(intensity). Each effect is a separate tsParticles instance so they layer independently.
particleConfigs.ts (new)
Pure functions — no side effects, no imports from React. Each takes intensity: number (0–100) and returns a tsParticles ISourceOptions config.
| Effect | Count range | Speed range | Particle style |
|---|---|---|---|
| Fire | 20–200 | 2–8 upward | Small circles, orange/red/yellow gradient, fade as they rise, emitter along bottom edge |
| Rain | 50–500 | 5–20 downward | Thin elongated lines, ~70° angle, slight opacity, full-width emitter at top |
| Embers | 5–80 | 0.2–1.5 drift | Small dots, warm amber, slow random drift, gentle fade-out |
FogOverlay.tsx (unchanged)
Props unchanged: active: boolean. CampaignView passes atmosphere.fog.active.
State
In CampaignView.tsx, replace fogActive: boolean with:
const [atmosphere, setAtmosphere] = useState({
fog: { active: false, intensity: 50 },
fire: { active: false, intensity: 50 },
rain: { active: false, intensity: 50 },
embers: { active: false, intensity: 50 },
});
Socket
Payload change
atmosphere:update payload changes from { fog: boolean } to:
{
fog: { active: boolean; intensity: number };
fire: { active: boolean; intensity: number };
rain: { active: boolean; intensity: number };
embers: { active: boolean; intensity: number };
}
Server (server/src/socket.ts)
Update the TypeScript type for the atmosphere:update handler to accept and rebroadcast the new payload shape. No persistence — server is a pure relay.
Client (CampaignView.tsx)
The atmosphere:update listener replaces the entire atmosphere state with the received payload, keeping all clients in sync including intensity values.
File Checklist
| File | Action |
|---|---|
client/src/components/AtmospherePanel.tsx |
Create |
client/src/components/AtmospherePanel.module.css |
Create |
client/src/components/ParticleOverlay.tsx |
Create |
client/src/lib/particleConfigs.ts |
Create |
client/src/components/FogOverlay.tsx |
No change |
client/src/components/FogOverlay.module.css |
No change |
client/src/pages/CampaignView.tsx |
Update state, socket listener, JSX |
server/src/socket.ts |
Update TypeScript type |
client/package.json |
Add @tsparticles/react, @tsparticles/slim |
Out of Scope
- Fog intensity wiring (slider exists, does nothing in v1)
- Moving atmosphere control to App-level header (deferred to DM view redesign)
- Snow or other effects beyond fog/fire/rain/embers
- Per-effect configuration beyond intensity (e.g. wind direction for rain)