darkwatch/docs/superpowers/specs/2026-04-10-particle-effects-design.md
Aaron Wood 646a16cd0f Add particle effects / atmosphere panel design spec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 18:54:44 -04:00

4.5 KiB
Raw Blame History

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 (0100).

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 (0100), 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 (0100) and returns a tsParticles ISourceOptions config.

Effect Count range Speed range Particle style
Fire 20200 28 upward Small circles, orange/red/yellow gradient, fade as they rise, emitter along bottom edge
Rain 50500 520 downward Thin elongated lines, ~70° angle, slight opacity, full-width emitter at top
Embers 580 0.21.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)