diff --git a/docs/superpowers/plans/2026-06-29-matrix-editor-refactor.md b/docs/superpowers/plans/2026-06-29-matrix-editor-refactor.md new file mode 100644 index 0000000..da30f35 --- /dev/null +++ b/docs/superpowers/plans/2026-06-29-matrix-editor-refactor.md @@ -0,0 +1,466 @@ +# Matrix Instrument + Colour Editor Refactor — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the tangled per-voice instrument + per-(voice,colour) modal editor with an inline-accordion, tabbed voice editor (adaptive pitch/drum step sequencer + synth/FX tab), split `control.js` into ES modules, and extend the SC engine with per-colour `res`/`rev`. + +**Architecture:** Three phases. **A** extends `matrix.scd` (res/rev colour-def fields) — backend, testable headless. **B** splits `control.js` (1151 lines) into ES modules with **behaviour unchanged** (the old modal still works) — pure refactor, verifiable by smoke test. **C** replaces the modal with the inline accordion + tabs + adaptive step editor + synth/FX tab. Each phase ships working software. + +**Tech Stack:** Vanilla JS ES modules (no bundler/framework/build step), Node `--test` for pure-logic unit tests, Playwright (Chrome) for browser smoke, SuperCollider (`sclang`) headless for engine tests. WebSocket→OSC bridge via `web_realart/server.js`. + +## Global Constraints + +- Vanilla **ES modules only** — no bundler, no framework, no build step. `index.html` loads `` with ``. The `matrix_glow.js` import line (l.189) stays. + +- [ ] **Step 4: Browser smoke test** + +Run the bridge (`cd web_realart && node server.js`) with SC booted (or stub), open `http://localhost:4400/control/`. Verify: page loads with no console errors, tabs switch, matrix grid renders, clicking a cell sends `/matrix/cell` (check Network/console), the colordef modal still opens. Expected: identical behaviour to before. + +- [ ] **Step 5: Commit** + +```bash +git add web_realart/public/control/ +git commit -m "refactor: split osc transport into es module" +``` + +### Task B2: Extract matrix-state.js (data, palette, persistence, classification) + +**Files:** +- Create: `web_realart/public/control/js/matrix-state.js` +- Modify: `js/main.js` (remove the moved declarations; import from matrix-state) +- Test: `web_realart/test/matrix-state.test.mjs` + +**Interfaces:** +- Produces: `export const MATRIX_VOICES` (22), `MATRIX_BARS = 64`, `MATRIX_INST_CHOICES`, `MATRIX_MOD_SOURCES`, `MATRIX_MOD_TARGETS`, `PALETTE` (`['#c22','#1a8','#25b','#990','#c60','#82a']`), `VOICE_CLASS` (`{kick:'drum', acid:'melodic', perc:'semi', …}`), `mkColor()`, `matGrid`, `matInst`, `matColorDefs` (exported as mutable via accessor object `state`), `saveMatState()`, `loadMatState()`, `isMelodic(voice)`, `defaultStepMode(voice)` (`'pitch'|'drum'`). + +- [ ] **Step 1: Write the failing unit test** + +```js +// web_realart/test/matrix-state.test.mjs +import { test } from "node:test"; +import assert from "node:assert"; +import { PALETTE, VOICE_CLASS, defaultStepMode, MATRIX_VOICES, MATRIX_BARS } from "../public/control/js/matrix-state.js"; + +test("palette is the single 6-colour source", () => { + assert.deepEqual(PALETTE, ["#c22","#1a8","#25b","#990","#c60","#82a"]); +}); +test("dimensions are 22x64", () => { + assert.equal(MATRIX_VOICES.length, 22); + assert.equal(MATRIX_BARS, 64); +}); +test("step-mode defaults by voice class", () => { + assert.equal(defaultStepMode("acid"), "pitch"); + assert.equal(defaultStepMode("kick"), "drum"); + assert.equal(defaultStepMode("perc"), "drum"); // semi defaults to drum +}); +``` + +- [ ] **Step 2: Run it, verify it fails** + +Run: `cd web_realart && node --test test/matrix-state.test.mjs` +Expected: FAIL — cannot import (module not created yet). + +- [ ] **Step 3: Create matrix-state.js** + +Move from `main.js` l.288-367 verbatim: `MATRIX_VOICES`, `MATRIX_INST_CHOICES`, `MATRIX_MOD_SOURCES/TARGETS`, `matInst`, `matColorDefs`, `mkColor`, `matGrid`, `MATRIX_BARS`, `saveMatState`, `loadMatState` (drop `cellRefs`/`matPlayhead`/`cdEditVoice` — those stay UI-side). Add and export: + +```js +export const PALETTE = ["#c22","#1a8","#25b","#990","#c60","#82a"]; +export const VOICE_CLASS = { + kick:"drum", hats:"drum", clap:"drum", perc:"semi", sub:"melodic", acid:"melodic", + arp:"melodic", lead:"melodic", stab:"melodic", pad:"melodic", ride:"drum", rim:"drum", + tom:"semi", reese:"melodic", bells:"melodic", sweep:"drum", melody:"melodic", + chord:"melodic", fx:"drum", snare:"drum", crash:"drum", shaker:"drum", +}; +export function isMelodic(v) { return VOICE_CLASS[v] === "melodic"; } +export function defaultStepMode(v) { return VOICE_CLASS[v] === "melodic" ? "pitch" : "drum"; } +``` +Make the mutable arrays exported `let` with named exports, or wrap in an exported `state` object — pick the pattern `main.js` can consume cleanly. Add `res:null, rev:null` to `mkColor()`'s returned shape, and back-fill them in `loadMatState` alongside steps/pose/mod. + +- [ ] **Step 4: Run the test, verify it passes** + +Run: `cd web_realart && node --test test/matrix-state.test.mjs` +Expected: PASS (3 tests). + +- [ ] **Step 5: Import into main.js, smoke test browser** + +Replace moved declarations in `main.js` with `import { … } from "./matrix-state.js";`. Browser smoke (as B1 Step 4): identical behaviour. + +- [ ] **Step 6: Commit** + +```bash +git add web_realart/public/control/js/matrix-state.js web_realart/public/control/js/main.js web_realart/test/matrix-state.test.mjs +git commit -m "refactor: extract matrix state and palette module" +``` + +### Task B3: Single palette via CSS custom properties + +**Files:** +- Modify: `web_realart/public/control/control.css` (`.mcell.m1..m6` l.109-114; `.cd-row.m*`/`.seq-row.m*` l.178-192) +- Modify: `js/matrix-state.js` (write CSS vars from `PALETTE` at load) + +**Interfaces:** Consumes `PALETTE` from matrix-state. Produces CSS vars `--m1..--m6` on `:root`. + +- [ ] **Step 1: Emit CSS vars from PALETTE** + +In `matrix-state.js`, add and call once on import: + +```js +export function installPaletteVars() { + const r = document.documentElement.style; + PALETTE.forEach((c, i) => r.setProperty(`--m${i + 1}`, c)); +} +installPaletteVars(); +``` + +- [ ] **Step 2: Replace hard-coded colours in CSS with vars** + +`control.css`: change `.mcell.m1 { background:#c22; border-color:#e44; }` … to `.mcell.m1 { background:var(--m1); border-color:var(--m1); filter:brightness(1.2); }` for m1..m6 (keep a readable border via `brightness`). Do the same for `.cd-row.m*` / `.seq-row.mN .seq-cell.on`. Remove the now-duplicate hex values. + +- [ ] **Step 3: Remove the JS COLORS array** + +In the (now in voice-editor / still in main) `renderColorSel`, replace the local `const COLORS = [...]` with `import { PALETTE } from "./matrix-state.js"` usage. (If still in main.js at this point, import there.) + +- [ ] **Step 4: Browser smoke** — cells m1..m6 and the colour selector render with identical colours. Expected: visually unchanged. + +- [ ] **Step 5: Commit** + +```bash +git add web_realart/public/control/ +git commit -m "refactor: single colour palette via css vars" +``` + +### Task B4: Extract mixer.js, transport.js, scenes.js, sequencer.js, launchpad.js + +**Files:** +- Create: `js/mixer.js`, `js/transport.js`, `js/scenes.js`, `js/sequencer.js`, `js/launchpad.js` +- Modify: `js/main.js` (import + call each module's `init()`) + +**Interfaces:** Each module exports `export function init()` that renders its DOM and registers its own `on(...)` OSC handlers and DOM event listeners, importing `send/on` from `./osc.js` and shared data from `./matrix-state.js`. No module reaches into another's internals. + +- [ ] **Step 1: Extract mixer.js** — move `mixerLevel`, `mixerMuted`, `renderMixer` (main.js l.711-772) + its `/launch/vol` sends; expose `export function init()`. (Most isolated — own state.) +- [ ] **Step 2: Extract transport.js** — beat/rms/tempo handlers (l.54-74) + quant/filter buttons; register `on("/sync/beat")`, `on("/sync/rms")`. +- [ ] **Step 3: Extract scenes.js** — `sceneMode`, `setSceneMode`, `/scene/state` handler, `[data-scene]` wiring (l.120-129, 1093-1111). +- [ ] **Step 4: Extract sequencer.js** — rhythm+melody editors (l.76-118, 830-973, 1011-1027, 1068-1091) + its `avlive.seq` persistence + `/seq/*` handlers. +- [ ] **Step 5: Extract launchpad.js** — `armed`, `clipState`, `togglePad`, `/armed/` handler, `[data-pad]` wiring (l.8-28, 42-52, 1029-1066). +- [ ] **Step 6: Wire in main.js** — `import` each, call `init()` in the `DOMContentLoaded` bootstrap. Keep tab switching + `[data-osc]` generic buttons in main.js. +- [ ] **Step 7: Browser smoke** — every tab and control works identically (mixer faders, transport, scenes, sequencer pads, launchpad clips). +- [ ] **Step 8: Commit** + +```bash +git add web_realart/public/control/ +git commit -m "refactor: extract mixer transport scenes seq pads" +``` + +### Task B5: Extract matrix-grid.js and voice-editor.js (modal as-is) + +**Files:** +- Create: `js/matrix-grid.js` (grid render, `applyMatCellColor`, `cellRefs`, glow, timeline — l.369-448, 610-709, 774-828) +- Create: `js/voice-editor.js` (the current modal: `renderColorSel`, `renderPattern`, `openColorDef`, `closeColorDef`, `setCD` + `cdEditVoice/cdColor/cdAuditioning` + the `/matrix/colordefs|steps|colormod|colorpose` handlers — l.450-608, 235-285) +- Modify: `js/main.js` (import + init; main.js is now just bootstrap + tabs + generic buttons) + +**Interfaces:** +- `matrix-grid.js`: `export function renderMatrix()`, `export function init()`, `export function triggerGlow(...)`; owns `cellRefs`, `matPlayhead`. `renderMatrix` calls `voiceEditor.openFor(vi)` on the edit button (import). +- `voice-editor.js`: `export function init()`, `export function openFor(vi)`, `export function isOpenFor(vi)` (replaces the `!getElementById('colordef-modal').hidden` checks). Registers its own OSC handlers. + +- [ ] **Step 1: Extract matrix-grid.js** with the exports above; replace the inbound matrix handlers it needs (`/matrix/cell`, `/matrix/grid`, `/matrix/instrument(s)`) with `on(...)` registrations inside its `init()`. +- [ ] **Step 2: Extract voice-editor.js** moving the modal functions verbatim; replace the 5 modal-DOM open-checks with `isOpenFor(vi)`; register `/matrix/colordefs|steps|colormod|colorpose` here. +- [ ] **Step 3: main.js final shape** — imports osc, matrix-state, and every module; bootstrap calls each `init()`; retains tab switching + `[data-osc]` wiring only. Confirm `main.js` is now < ~120 lines. +- [ ] **Step 4: Full browser smoke** — grid, playhead, glow, timeline drag, mixer, and the **modal editor** (open, colour select, steps, mod, pose, audition) all behave identically. +- [ ] **Step 5: Keep matrix_glow test green** — Run: `cd web_realart && node --test test/matrix_glow.test.mjs` → PASS. +- [ ] **Step 6: Commit** + +```bash +git add web_realart/public/control/ +git commit -m "refactor: extract matrix grid and voice editor" +``` + +--- + +## Phase C — New editor UX + +Phase C changes behaviour. After B, `voice-editor.js` is the only file that defines the editor — all C work is contained there + `index.html` markup + `control.css`. + +### Task C1: Inline accordion (replace the modal) + +**Files:** +- Modify: `index.html` (remove `#colordef-modal` block l.180-188; the editor now renders into an expandable row inside `#matrix-grid`) +- Modify: `js/matrix-grid.js` (grid row gains the unified instrument dropdown + a `▷` expand toggle; renders an editor container row under the expanded voice) +- Modify: `js/voice-editor.js` (`openFor(vi)` renders into the inline container, not the modal; track single `expandedVoice`) +- Modify: `control.css` (drop `.cd-modal`/`.cd-panel`; add `.voice-editor-row`, `.voice-expand-btn`) + +**Interfaces:** `voice-editor.js`: `openFor(vi, containerEl)` renders into `containerEl`; `collapse()` clears it; only one voice expanded (opening another collapses prior). The per-voice instrument dropdown stays in the grid row (single source) and is the only instrument selector. + +- [ ] **Step 1: Grid row markup** — in `renderMatrix`, after the instrument dropdown add a ``; below each voice row, an empty `