Release 4 — 2D Effects & Layered Composition (original plan)¶
Theme: Release 3 brought the pixel pipeline to life. Release 4 steps up to 2D: multi-dimensional layouts, 2D effects, and the real layer system — concurrent
VirtualLayers blending additively, each with its own bounds and palette.
See moonlight-scope/index.md for overall coverage status.
Release Overview¶
| Gap | Addressed by | Status |
|---|---|---|
| Only 1D strips; no matrix support | Sprints 1–3 (2D layouts) | Sprint 1 ✅, 2–3 pending |
| No 2D effects | Sprints 4–5 (2D effects) | Ripples ✅, rest pending |
| Single layer only — no blending | Sprints 6–7 (compositing) | Sprint 6 ✅, Sprint 7 pending |
| Layers can't be bounded | Sprint 8 (startPct/endPct) |
✅ Done |
| No palette system | Sprint 9 (palettes) | Pending |
| No way to reorder / solo / mute layers | Sprint 10 (per-layer UI polish) | Reorder ✅, rest pending |
Out of scope: 3D (R5), modifiers (R5), parallel drivers (R6).
Sprint 1 — 2D Layout: Panel (Serpentine) ✅¶
Done. Delivered as GridLayout with physical coordinate mapping and serpentine wiring in actual Release 3 Sprint 6. See Release 3 Sprint 6.
GridLayout supports width, height, serpentine flag, and Coord3D blend loop in DriverLayer. Four serpentine combinations covered in tests.
Sprint 2 — 2D Layout: Multi-Panel Grid¶
Scope¶
Goal: compose multiple LayoutPanels into a single large virtual canvas — each panel with its own pin, its own wiring direction, its own (x,y) offset.
Part A — LayoutMultiPanel. Children are LayoutPanel instances. Parent resolves global (x,y) by summing the child's offset + local (x,y).
Part B — Per-panel pin claim. Each child panel claims its own led_data pin via IoModule. Useful for parallel output (driven properly in R6).
Definition of Done:
- Four 16×16 panels arranged as a 32×32 canvas
- Each panel on its own pin
SolidEffectModulerenders a single color across the full 32×32 — panels indistinguishable from a logical single panel
Result¶
To be completed after implementation.
Retrospective¶
To be completed after implementation.
Sprint 3 — 2D Layout: Rings and Wheel¶
Scope¶
Goal: two more 2D layout shapes for common fixture geometries.
Part A — LayoutRings. Concentric rings; controls: rings (count), pixelsPerRing (array or formula). Maps (x,y) to the nearest ring+angle.
Part B — LayoutWheel. Single radial strip fanning out from center; controls: spokes, pixelsPerSpoke. Useful for clock-face or starburst fixtures.
Definition of Done:
- Both layouts spawnable as children of
VirtualLayer - Coordinate test: for a
LayoutRingswith 3 rings × 16 pixels, a virtual (x=0,y=0) write lights the innermost pixel at angle 0 - Documented in
docs/modules/layouts.mdwith a diagram per layout
Result¶
To be completed after implementation.
Retrospective¶
To be completed after implementation.
Sprint 4 — 2D Effects: Noise, Ripples, Distortion¶
Partially done. RipplesEffect ported from MoonLight in actual Release 3 Sprint 1. See Release 3 Sprint 1.
Still pending:
NoiseEffect2D— Perlin noise in (x,y,t) space; controls:scale,speed,paletteDistortionWaves2D— WLED-ported interfering sine waves on both axes
Sprint 5 — 2D Effects: Blackhole, Lissajous, Game of Life¶
Scope¶
Goal: three more 2D effects that showcase different shape primitives.
Part A — BlackholeEffect. Radial inward pull with swirl; WLED-ported.
Part B — LissajousEffect. Parametric curve; controls: freq_x, freq_y, phase.
Part C — GameOfLifeEffect. Conway's rules; one generation per tick. Controls: seed, wraparound, color. Resets when the grid dies.
Definition of Done:
- Three effects registered and visually verified
GameOfLifeEffecttest: glider pattern propagates diagonally over 4 generations- Footprint delta measured: ≤ 10 KB flash total for all three
Result¶
To be completed after implementation.
Retrospective¶
To be completed after implementation.
Sprint 6 — Layer Compositing: Additive Blend¶
Partially done. Saturating additive blend (ConsumerLayer blending fix) delivered in actual Release 3 Sprint 1. See Release 3 Sprint 1.
Still pending:
- Per-layer
brightnesscontrol (opacity before the sum — cheap crossfade) - Core 1 FreeRTOS driver task dispatch (infrastructure wired in R2 Sprint 4; not yet dispatched)
Sprint 7 — Multi-Layer Coexistence + Fade-In¶
Scope¶
Goal: raise the concurrent-layer cap to 4 and smooth new-layer activation with a 500 ms fade-in.
Part A — 4-layer cap enforced. PhysicalLayer rejects a 5th child with a clear error; UI "add layer" button grays out at the cap. Cap is a constexpr for easy bump in R5.
Part B — Layer fade-in. When a new layer is added, its brightness ramps from 0 → target over 500 ms (25 ticks at 50 Hz). Quadratic easing. No audible pop when an audio-reactive effect joins.
Part C — Teardown fade-out. Symmetric: removing a layer fades it out over 500 ms before the module is actually destroyed.
Definition of Done:
- 4 layers running simultaneously on 16×16 panel, each with a different effect
- Add-a-layer: brightness trace captured in a unit test shows ramp 0→target over 25 ticks
- Footprint: 4 × 256-pixel layers fit in ESP32-S3 PSRAM budget with ≥ 50% headroom
Result¶
To be completed after implementation.
Retrospective¶
To be completed after implementation.
Sprint 8 — startPct / endPct per Layer ✅¶
Done. EffectsLayer start/end persistence (Coord3D bounding) delivered in actual Release 3 Sprint 6. See Release 3 Sprint 6.
startPct/endPct as percentage-based bounds are persisted per EffectsLayer; the setPixel path clips writes to the declared range.
Sprint 9 — Palette System¶
Scope¶
Goal: effects stop hardcoding colors; they sample from a selectable palette.
Part A — PaletteModule. Wraps FastLED's CRGBPalette16 (or 32). Preset palettes: RainbowColors, PartyColors, CloudColors, LavaColors, OceanColors, ForestColors, HeatColors. Stored as compile-time tables (no allocation).
Part B — Global palette. LightsControlModule gains a palette dropdown listing all registered palettes. All effects that opt in read from this palette by default.
Part C — Per-effect palette override. Each effect's optional palette control — empty means "use global", set means "use this one." Frontend shows a small swatch strip next to the dropdown.
Part D — Refactor R3 + R4 effects. RainbowEffectModule, NoiseEffect2D, RipplesEffect2D, BlackholeEffect all switched to palette sampling.
Definition of Done:
- 7 preset palettes registered
- Changing the global palette changes all palette-driven effects immediately
- Per-effect override works: layer 1 uses lava, layer 2 uses ocean, both rendered correctly
Result¶
To be completed after implementation.
Retrospective¶
To be completed after implementation.
Sprint 10 — Per-Layer UI Polish¶
Partially done. Drag-reorder of module cards delivered in actual Release 3 Sprint 2. See Release 3 Sprint 2.
Still pending:
- Solo / mute toggles ("S" and "M" buttons — classic DAW semantics)
- Rename + color tag (free-text layer name; 8 preset color tags)
- Collapsed card state (single row: name + tag + S/M/power; expand-all / collapse-all)
Release 4 Backlog¶
- Up to 16 concurrent layers. Raised cap — revisit in R5 when modifiers arrive and each layer may carry transform state.
- Layer groups. Folder-like grouping for many layers. Pick up if operator UX complaints emerge around 8+ layers.
- Bounded-layer efficient pixel iteration. Currently every layer iterates the full canvas and clips in
setPixel; for bounded layers we could iterate only the bounded range. Pick up if profiling shows this is hot. - Palette editor UI. Edit a 16-color palette from the frontend. Pick up when demand is real — preset palettes cover 90% of use cases.