Skip to content

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
  • SolidEffectModule renders 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 LayoutRings with 3 rings × 16 pixels, a virtual (x=0,y=0) write lights the innermost pixel at angle 0
  • Documented in docs/modules/layouts.md with 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, palette
  • DistortionWaves2D — 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
  • GameOfLifeEffect test: 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 brightness control (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.