745 lines
48 KiB
Markdown
745 lines
48 KiB
Markdown
# Mix Visualizer — Phase 10 Reframe (Lava) — Design Spec
|
||
|
||
Status: **design-complete, implementation-ready.** Author: product-designer. Date: 2026-06-16.
|
||
**No code has been written by this doc.**
|
||
|
||
This is a **major reframe of the Mix visualizer's effects layer, controls, and color model**, folded
|
||
**under the still-open Phase 10** (the WebGL2 renderer) as a **reframe wave-set (Waves R1–R4)** —
|
||
distinct from Phase 10's original Waves 1–4 (renderer swap, controls row, the four effects, the
|
||
popover/knob polish) which are **already merged**. It builds on that landed Phase 10 infrastructure (the
|
||
single-pass fragment-shader pipeline, the loudness datum texture, the wall-clock playhead interpolation,
|
||
the controls UI, the widened Mix body) and **replaces what that pipeline paints** — the per-bar bulge,
|
||
the analytic-metaball "lava," the glass treatment, and the navy↔moss color treatment. The renderer
|
||
*infrastructure* is reused; the *art and the controls* are rebuilt.
|
||
|
||
> **Numbering note.** This reframe lives **under Phase 10**, not as a separate phase. Its waves are
|
||
> labelled **R1–R4** to stay unambiguous against Phase 10's landed Waves 1–4. The filename
|
||
> `phase-10-mix-visualizer-lava-reframe.md` reflects that; an earlier draft of this doc was numbered
|
||
> "Phase 12" — that numbering is retired.
|
||
|
||
**Why a reframe, not an iteration.** Daniel tested the landed Phase 10 effects end-to-end and rejected
|
||
the visual result: the lava reads as "giant disconnected circles," the colors drifted to cyan (an HSL
|
||
saturation-boost artifact — see §6), and the waveform and the lava read as two unrelated things sharing
|
||
a canvas rather than one coherent fluid surface. The diagnosis (from a staff-engineer research pass) is
|
||
that the rejected look is *structural to the current effect approach*, not a tuning miss: too few
|
||
scripted blobs with no physics produce disconnected circles, and HSL interpolation between two
|
||
low-saturation theme tokens passes through hue regions that read as cyan. Both are fixed by changing the
|
||
*model*, not the dials.
|
||
|
||
This spec **supersedes** the original Phase 10 effects/controls/color design:
|
||
|
||
- `product-notes/mix-visualizer-webgl-renderer.md` **§4 (the four visual effects)** — superseded by
|
||
§3–§6 here.
|
||
- `product-notes/mix-visualizer-webgl-renderer.md` **§7 (Wave 4 popover-controls rework)** — superseded
|
||
by §7 here (the trigger and the widened body are kept; the four-knob popover becomes a six-knob
|
||
inline collapse/expand knob-bar — see §7).
|
||
|
||
What carries forward unchanged from Phase 10's landed waves (do **not** re-derive — reference it):
|
||
|
||
- The single-pass WebGL2 fragment renderer, the full-window quad, the trivial pass-through vertex
|
||
shader (`mix-visualizer-webgl-renderer.md` §2a).
|
||
- The loudness datum as a GPU texture, sampled per-fragment (§2b; the 2-D grid + `texelFetch` manual
|
||
interpolation that the landed `MixVisualizer.ts` already implements).
|
||
- The wall-clock playhead interpolation + the netcode-style correction-offset smoothing (the landed
|
||
`PLAYHEAD_CORRECTION_*` machinery).
|
||
- The Blazor↔JS bridge contract (`MixWaveformVisualizer.razor.cs`) — `create` → handle with
|
||
`setDatum` / `setPlayback` / `setZoom` / `refreshTheme` / `dispose`, the idempotent datum-push guard,
|
||
the `IsActivePlayer` gating, the rAF loop gated on `isPlaying` (§2d, §2e).
|
||
- The `MIN_VISIBLE_SECONDS = 0.333 s` max-zoom anchor (one quarter note at 180 BPM) and the
|
||
`MixZoomMapping` log-space fraction↔seconds mapping.
|
||
- The read-only contract (8.K §D): one-way playback input, no seek, no scrub, no write-back.
|
||
- The widened Mix body (`MudContainer MaxWidth="Large"`) and the lava-lamp `DDIcons.LavaLamp`
|
||
icon-button trigger top-right of the body, across from the back link (Phase 10 §7c, §7g — **kept**;
|
||
the icon glyph itself is **redrawn** by this reframe, see §7f).
|
||
|
||
Cross-references (read these before implementing):
|
||
|
||
- `product-notes/mix-visualizer-webgl-renderer.md` — the Phase 10 renderer spec this reframes. §1/§2
|
||
(scope, renderer architecture, bridge) carry forward; §4/§7 are superseded.
|
||
- `DeepDrftPublic/Interop/visualizer/MixVisualizer.ts` — the landed renderer. The Wave 1 scroll/zoom
|
||
geometry, the datum texture, and the playhead machinery are reused; the §4-effect GLSL (the bubble
|
||
SDF, the detach blobs, the HSL `mixHsl`/`vivify` color, the glass) is the part being replaced.
|
||
- `DeepDrftPublic.Client/Controls/MixVisualizerControls.razor[.cs]` — the four-knob control component;
|
||
becomes six knobs in an inline collapse/expand bar (§7).
|
||
- `DeepDrftPublic.Client/Services/MixVisualizerControlState.cs` — the scoped four-property state holder;
|
||
widens to six properties (§7c).
|
||
- `DeepDrftPublic.Client/Controls/MixZoomMapping.cs` — reused unchanged for the scroll-speed control.
|
||
- `DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor[.cs/.css]` — the bridge. Extend the handle
|
||
with the new control setters; the `.css` gains the overflow-clip work (§2).
|
||
- `DeepDrftPublic.Client/Pages/MixDetail.razor[.css]` — the page; the controls area gains the inline
|
||
collapse/expand knob-bar (§7).
|
||
- `DeepDrftPublic.Client/Pages/SessionDetail.razor[.css]` — the **NowPlaying / hero aesthetic** the
|
||
knob-bar must match (§7e).
|
||
- `DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor[.css]` — the footer/player bar
|
||
whose **dynamic height** the clip line must follow (§2c).
|
||
- `DeepDrftShared.Client/Common/DeepDrftPalettes.cs` — the **single source of truth** for theme colors
|
||
(§6a). `DeepDrftShared.Client/Components/RadialKnob.razor` — the knob, consumed unchanged (§7d).
|
||
- `DeepDrftShared.Client/Common/DDIcons.cs` — the lava-lamp glyph, **redrawn** by this reframe (§7f).
|
||
|
||
---
|
||
|
||
## 1. Goal and scope boundary
|
||
|
||
**Goal.** Replace the rejected lava/bulge/glass/color treatment with a **credible physical lava-lamp**:
|
||
~16–32 simulated wax blobs whose motion is integrated on the CPU each frame and rendered as smooth
|
||
metaballs in the existing fragment shader, sharing the *same plane* as the waveform **with real 2D
|
||
collision** — the waveform pushes the fluid out of its way. Replace the navy↔moss treatment with a
|
||
**three-color, OKLab-interpolated, per-segment-baked gradient** that animates along three combined
|
||
motions. Replace the four-knob popover with a **six-knob inline collapse/expand knob-bar** styled to
|
||
match the hero NowPlaying aesthetic. Remove the static noise texture that makes the screen look dirty.
|
||
Redraw the lava-lamp trigger glyph to the classic 1970s silhouette (§7f).
|
||
|
||
**In scope.**
|
||
|
||
- A small **CPU-side per-frame physics step** modeling ~16–32 Lagrangian wax blobs (position, velocity,
|
||
temperature, radius), uploaded to the shader as a uniform array / tiny data texture each frame (§4).
|
||
- A **2D collision model**: blob↔waveform-boundary and blob↔blob, elastic, with a tunable hardness knob
|
||
that blends soft-displacement → hard-obstacle (§5).
|
||
- The **three-color OKLab gradient model** with three combined motions: anchor rotation among X/Y/Z,
|
||
per-segment sinusoidal variation baked at segment entry, and the per-bar curve shift with scroll
|
||
height (§6).
|
||
- **Six controls** replacing the four: scroll speed, gradient rotation speed, lava gravity, lava heat,
|
||
blob density/size, collision strength (§7).
|
||
- The **inline collapse/expand knob-bar** (replacing the popover) styled to the NowPlaying hero
|
||
aesthetic (§7).
|
||
- A **redrawn `DDIcons.LavaLamp`** glyph — classic 1970s lava-lamp silhouette (§7f).
|
||
- **Overflow clipping** to the dynamic footer height (§2).
|
||
- **Removing the static noise texture** (§3).
|
||
|
||
**Out of scope / unchanged.**
|
||
|
||
- **No playback-control changes.** Read-only contract holds (8.K §D). One-way playback input; no seek,
|
||
no scrub, no write-back. No control is a seek surface.
|
||
- **No datum format change.** The duration-derived ~333 samples/sec loudness datum (8.K §F) and the
|
||
GPU-texture upload path are reused as-is. The waveform geometry (symmetric ±amplitude about a center
|
||
line, rising from the bottom) is reused; this spec changes only the *clip*, the *collision role*, and
|
||
what the fluid does around it.
|
||
- **No bridge redesign.** Extend the handle with the new control setters; preserve the single-owner
|
||
bridge, the idempotent datum guard, the `IsActivePlayer` gating, and the `isPlaying`-gated rAF loop.
|
||
- **No renderer-tech change.** Stay single-pass WebGL2 fragment shader + a CPU physics step. **Do NOT
|
||
build a ping-pong FBO Navier-Stokes fluid sim** (§4, "rejected alternative").
|
||
|
||
**Reused vs. replaced — at a glance.**
|
||
|
||
| Layer | Reused from Phase 10 (Waves 1–4) | Replaced by this reframe (R1–R4) |
|
||
|-------|----------------------------------|----------------------------------|
|
||
| WebGL2 pipeline / quad / vertex shader | ✅ as-is | — |
|
||
| Datum texture + sampling | ✅ as-is | — |
|
||
| Playhead interp + smoothing | ✅ as-is | — |
|
||
| Bridge contract + lifecycle | ✅ (extend setters) | — |
|
||
| Scroll/zoom geometry | ✅ as-is (now scroll-speed-driven, §7) | — |
|
||
| Waveform silhouette | ✅ (now also a collision boundary, §5) | — |
|
||
| Lava effect (bulge + detach blobs) | — | CPU-physics wax blobs + metaballs (§4) |
|
||
| Color model (HSL navy↔moss) | — | OKLab three-color gradient, 3 motions (§6) |
|
||
| Glass treatment | — | folded into the blob shading (§4f); no separate glass dials |
|
||
| Static noise/frost texture | — | **removed** (§3) |
|
||
| Controls (4 knobs, popover) | — | 6 knobs in an inline collapse/expand bar (§7) |
|
||
| Lava-lamp trigger glyph | trigger placement kept | glyph **redrawn** (§7f) |
|
||
|
||
---
|
||
|
||
## 2. Spatial model & layout
|
||
|
||
### 2a. Same plane, with collision
|
||
|
||
The waveform and the lava **occupy the same plane**. The waveform is not a backdrop the lava floats over
|
||
— it is a **physical collision boundary** the fluid is pushed out of by. As the waveform's silhouette
|
||
rises and falls (its half-width is the loudness at each scroll row), wax blobs that drift into that
|
||
silhouette are pushed away from it. The interaction is two-way in *appearance* (the fluid visibly
|
||
parts around the waveform) but one-way in *authority* (the waveform shape is driven by the datum +
|
||
playback, never by the fluid — the read-only contract holds; the fluid never deforms the waveform).
|
||
|
||
This is the headline fix for "the waveform and lava read as two unrelated things": they now share one
|
||
SDF-composited surface and one collision space, so the fluid demonstrably flows around the waveform.
|
||
|
||
### 2b. Bottom-anchored waveform (kept)
|
||
|
||
The waveform already rises from the bottom and scrolls bottom-to-top (8.K §A). **That stays.** New audio
|
||
enters at the bottom, played audio exits the top, the "now" line sits at a fixed screen Y. This reframe
|
||
does not touch the scroll geometry except to drive it from the new scroll-speed control instead of the
|
||
zoom control (§7).
|
||
|
||
### 2c. Overflow clip to the dynamic footer height
|
||
|
||
**The problem.** The visualizer canvas is `position: fixed; inset: 0` (full viewport). Visuals currently
|
||
bleed *past the footer* — the lava and waveform paint behind/over the player bar at the bottom of the
|
||
screen instead of stopping cleanly above it.
|
||
|
||
**The fix.** The visualizer must be **clipped so its visuals stop at the top edge of the footer bar**,
|
||
and the clip line is also the lava "rest" line (§5). The clip line must **follow the actual current
|
||
height of the footer**, which changes between states:
|
||
|
||
- **Player minimized:** the footer is a small floating FAB at `bottom: 30px` (`.minimized-dock` in
|
||
`AudioPlayerBar.razor.css`) — effectively no full-width bar; the clip line sits near the viewport
|
||
bottom.
|
||
- **Player expanded:** the footer is a full-width `MudContainer` + `MudPaper` surface
|
||
(`.player-surface`, `margin-bottom: 1rem`) whose height depends on the responsive grid layout
|
||
(transport/meta/seek/volume zones reflow across breakpoints — see the `AudioPlayerBar.razor.css`
|
||
grid-area media queries) and on whether an error `MudAlert` is showing.
|
||
|
||
So a **fixed inset is wrong** — the footer height is dynamic. The clip line must be measured from the
|
||
live footer, not hard-coded.
|
||
|
||
**Implementation requirement (flagged for staff-engineer).** The clip height is a *layout* value the
|
||
WebGL canvas needs as a *render* value. The recommended approach: the bridge observes the footer
|
||
element's height (a `ResizeObserver` on the player-bar root, or a CSS custom property the player bar
|
||
publishes that the visualizer reads) and pushes a `setFooterClip(heightPx)` uniform/scissor value to the
|
||
module; the shader (or a GL scissor/viewport) clips the bottom `heightPx` of the canvas to transparent,
|
||
and the lava rest line is computed from it. The player bar already routes all minimize/expand mutations
|
||
through one `SetMinimized` mutator and fires `OnMinimized` — that is a natural signal to recompute, but
|
||
a `ResizeObserver` is more robust because it also catches breakpoint reflow and the error-alert case.
|
||
Wrap the visualizer container in `overflow: hidden` regardless (the `.mix-waveform-bg` wrapper already
|
||
has it) so nothing bleeds during the measurement settle.
|
||
|
||
**Acceptance (§8):** with the player expanded, no waveform or lava pixel paints over or under the player
|
||
bar; minimizing the player drops the clip line and the lava rest line down to match; resizing the
|
||
window (changing the player-bar breakpoint/height) re-clips without a reload.
|
||
|
||
---
|
||
|
||
## 3. Remove the static noise texture
|
||
|
||
The current renderer applies a value-noise "frost" modulation across the whole ribbon
|
||
(`MixVisualizer.ts` `frost = 0.85 + 0.15 * valueNoise(...)` and the frosted-translucency glass layer).
|
||
Daniel: it makes the screen look dirty. **Remove it.** The waveform itself is otherwise fine once
|
||
de-noised — the de-noised waveform + the new lava + the new gradient are the visual.
|
||
|
||
This does **not** forbid noise *inside the physics* (e.g. a little organic jitter on blob shape or a
|
||
slow temperature field) — it forbids the *static screen-space dirt layer* that sits over everything. Any
|
||
remaining noise must be (a) tied to the moving fluid, not the screen, and (b) not read as a dirty
|
||
overlay. When in doubt, leave it out; the lava is the texture.
|
||
|
||
---
|
||
|
||
## 4. The lava system — CPU-physics wax blobs
|
||
|
||
### 4a. The model (from the research pass — the load-bearing recommendation)
|
||
|
||
Model the lava as **~16–32 physical "wax blobs" (Lagrangian metaballs)**. Each blob carries:
|
||
|
||
- **position** (2D, in the shared waveform/lava plane),
|
||
- **velocity** (2D),
|
||
- **temperature** (scalar — hot wax rises, cool wax sinks; drives the buoyancy that makes the lamp
|
||
"go"),
|
||
- **radius** (~20px–100px on screen; varied per blob).
|
||
|
||
Each frame, a **small CPU-side physics step** in JS integrates the blobs: buoyancy from temperature,
|
||
gravity (the gravity control), heat exchange (the heat control sets how much energy enters the system),
|
||
collisions (§5), and a little damping/viscosity so it reads as high-viscosity wax, not water. The blob
|
||
state is uploaded to the fragment shader each frame as a **uniform array** (`vec4 blobs[N]`-style:
|
||
`xy` = position, `z` = radius, `w` = a packed temperature/identity) or a **tiny data texture** if the
|
||
array bumps a uniform-count limit. The shader blends the blobs with a **smooth-min (`smin`) SDF
|
||
metaball** union — the same `smin` machinery the landed shader already has — plus the waveform SDF, and
|
||
shades the result.
|
||
|
||
This fixes "giant disconnected circles": the prior approach had too few scripted blobs (`DETACH_BLOB_COUNT
|
||
= 6`) with hash-driven pseudo-motion and no physics, so they read as detached discs. Real physics on
|
||
16–32 blobs with `smin` merging reads as continuous wax that splits and recombines.
|
||
|
||
### 4b. Blob shape — varied, organic, not always circular
|
||
|
||
Blobs range **~20px to ~100px** across and are **not always circular** — they should read as varied,
|
||
organic wax shapes. The `smin` union of overlapping circles already produces non-circular composite
|
||
shapes (two merging blobs form a peanut/neck); on top of that, a per-blob slight anisotropy or a
|
||
low-frequency radius modulation (tied to the blob, not the screen — see §3) gives each blob its own
|
||
organic silhouette. Staff-engineer's call on the exact shape primitive; the *intent* is "varied organic
|
||
wax," not "N identical circles."
|
||
|
||
### 4c. Heat = 0 → rest at the bottom; collision always on
|
||
|
||
- At **heat = 0**, the lava **rests at the bottom** (pooled at the clip/rest line, §2c). No bubbles rise
|
||
on their own. The **only** thing that moves the wax at heat 0 is **collision from the waveform** —
|
||
i.e. the waveform pushing through the resting pool displaces it. The waveform↔lava collision is
|
||
**always on, independent of heat** (§5).
|
||
- At **heat = max**, **many bubbles rise and morph per second** — a busy, actively roiling lamp.
|
||
|
||
The mapping from the 0..1 heat scalar to effect intensity (rise rate, how many blobs are buoyant at
|
||
once, churn frequency) is a **well-tuned transfer function that staff-engineer owns** — this spec does
|
||
not fix a formula. Note it as a tuning task: the requirement is the *endpoints* (0 = rest-at-bottom,
|
||
collision-only; max = many rising/morphing per second) and a smooth, good-feeling sweep between them.
|
||
|
||
### 4d. Gravity control
|
||
|
||
A separate **lava gravity** control sets the downward force on the wax. Higher gravity = wax falls back
|
||
faster, blobs are flatter at rest, rising requires more heat; lower gravity = wax floats more freely,
|
||
slower settling. Interacts with heat (buoyancy vs. gravity is the lamp's core tension) — staff-engineer
|
||
tunes the interplay; the control is an independent axis the user can dial.
|
||
|
||
### 4e. Blob density/size control (the fifth control)
|
||
|
||
A **blob density/size** control sets how much wax is in the system — the blob count within the ~16–32
|
||
band and/or the average radius within the ~20–100px band. Low = a few large lazy blobs; high = many
|
||
smaller active blobs. This is the realism/cost dial from the research pass (blob count is the
|
||
performance lever — see §4g).
|
||
|
||
### 4f. Shading — glass folded in, no separate dials
|
||
|
||
The Phase 10 spec had a separate four-part "glass" effect with its own tuning constants. In this reframe
|
||
the wax is shaded as **lit, translucent, glossy wax** as part of rendering the blobs — a surface normal
|
||
from the SDF gradient, a specular highlight, a soft Fresnel rim, and translucency over the page — but
|
||
this is **a property of how blobs are drawn, not a separate user control.** There is no glass knob. Keep
|
||
it tasteful and physical (wax is glossy-translucent, lit from a fixed virtual light); do **not**
|
||
reintroduce a screen-space frost/noise layer (§3) or any CPU `backdrop-filter`. Staff-engineer owns the
|
||
shading constants; the intent is "lit glassy wax," subordinate to the gradient color (§6), not competing
|
||
with it.
|
||
|
||
### 4g. Performance levers
|
||
|
||
- **Blob count is the realism/cost dial.** 16–32 is cheap; the per-fragment SDF loop over blobs is the
|
||
cost. Bound the loop (a hard `MAX_BLOBS` constant the shader loops to).
|
||
- **Keep the existing `MAX_DPR = 2` cap** as the graceful-degrade lever (drop internal resolution before
|
||
dropping frames — 8.K §E, carried forward).
|
||
- The CPU physics step is O(blobs²) for blob↔blob collision at worst (32² = 1024 pair checks/frame —
|
||
trivial) and O(blobs) for waveform collision; neither is a concern at this count.
|
||
|
||
### Rejected alternative (do not build): full fluid simulation
|
||
|
||
A ping-pong FBO Stable-Fluids / Navier-Stokes simulation was **considered and rejected.** A lava lamp is
|
||
high-viscosity / low-turbulence — the *opposite* regime from what a fluid solver buys you. It would be a
|
||
large rewrite (multi-pass FBO, advection/pressure-solve plumbing) for realism we explicitly do not want.
|
||
The Lagrangian-blob approach is both cheaper and a better match for the wax aesthetic. Keep the FBO fluid
|
||
sim as a *deliberate later upgrade only* if the blob approach ever reads too kinematic — and even then it
|
||
is a separate phase, not a tuning step.
|
||
|
||
---
|
||
|
||
## 5. The 2D collision model
|
||
|
||
### 5a. Two collision pairs, elastic
|
||
|
||
1. **Blob ↔ waveform boundary.** The waveform silhouette (the symmetric ±loudness ribbon about the
|
||
center line) is a collision boundary. A blob overlapping the silhouette is pushed out along the
|
||
waveform's surface normal — the fluid parts around the waveform. **2D elastic collision** against the
|
||
boundary (the blob's velocity component into the boundary reflects, modulated by the hardness knob —
|
||
§5c). The waveform is unaffected (read-only authority, §2a).
|
||
2. **Blob ↔ blob.** Blobs collide with each other — **2D elastic collision** (the standard
|
||
equal-or-unequal-mass elastic response along the line connecting centers), again modulated by
|
||
hardness. This keeps the wax from interpenetrating into one mush and gives the lamp its jostling
|
||
liveliness.
|
||
|
||
Mass can be derived from radius (bigger blob = more mass) so large blobs shove small ones convincingly —
|
||
staff-engineer's call; the requirement is *elastic 2D collision on both pairs.*
|
||
|
||
### 5b. Collision always on, independent of heat
|
||
|
||
Per §4c: the waveform↔lava collision runs **at all heat levels, including heat = 0.** At rest the
|
||
waveform still pushes through the pooled wax and displaces it. This is what keeps the waveform and the
|
||
lava feeling like one physical system rather than two layers.
|
||
|
||
### 5c. Collision strength — soft → hard (the sixth control)
|
||
|
||
A **collision strength** control sweeps the interaction from **soft → 100% hard.** It blends between two
|
||
behaviors:
|
||
|
||
- **(a) Hard obstacle:** the waveform is a rigid wall the fluid flows around — full elastic reflection,
|
||
blobs cannot enter the silhouette at all.
|
||
- **(b) Soft displacement/shove:** the waveform gently pushes the fluid aside — a soft penalty force
|
||
proportional to penetration depth, blobs squish against and partially into the boundary before being
|
||
eased out.
|
||
|
||
The knob blends (b) → (a) as it sweeps 0 → 1. At 0 it is a gentle shove (the fluid yields and slowly
|
||
recovers); at 1 it is a hard wall (crisp reflection, no penetration). The same blend factor can scale the
|
||
blob↔blob restitution for consistency (softer overall world at low strength), staff-engineer's call.
|
||
|
||
> **Dropped:** the earlier Phase 10 "bubbles spawn from peaks" idea is **not relevant** to this model —
|
||
> blobs are a persistent physical population, not spawned-from-the-waveform particles. Do not carry it
|
||
> forward.
|
||
|
||
### 5d. Transfer functions left to staff-engineer
|
||
|
||
As with heat (§4c), the exact penetration-penalty curve, restitution coefficients, and the soft↔hard
|
||
blend shape are **staff-engineer tuning tasks.** This spec fixes the *model* (two elastic collision
|
||
pairs, a soft↔hard blend on a knob, collision always on) and the *endpoints*, not the constants.
|
||
|
||
---
|
||
|
||
## 6. The color / gradient model
|
||
|
||
### 6a. One source of truth — injected, not hardcoded
|
||
|
||
**Do NOT duplicate or hardcode theme hexes** anywhere in the visualizer. The canonical palette lives in
|
||
**`DeepDrftShared.Client/Common/DeepDrftPalettes.cs`** (the `Light` / `Dark` `PaletteLight`/`PaletteDark`
|
||
objects). That is the single source of truth.
|
||
|
||
The component must receive the theme color(s) as a **single injected value/parameter** derived from that
|
||
source — not a second copy of the hexes living in the TS or the component. Today the renderer reads
|
||
computed `--mud-palette-*` CSS custom properties off the canvas element (because a GLSL uniform cannot
|
||
resolve `var()`), which is *a* form of single-source consumption (the vars are emitted from
|
||
`DeepDrftPalettes`). **Keep that discipline:** the colors reach the shader by reading the live palette
|
||
(via the CSS vars the palette emits, re-read on `refreshTheme`), so a palette edit in `DeepDrftPalettes`
|
||
or a dark-mode toggle re-themes the field with no duplicate to keep in sync. If staff-engineer prefers a
|
||
more explicit injection (the page passing the three resolved colors into the component as a typed
|
||
parameter sourced from `DeepDrftPalettes`), that is acceptable and arguably cleaner — the **hard
|
||
requirement is one source, zero hardcoded duplicates.**
|
||
|
||
**Identify the three colors X, Y, Z (§6b motion 1).** The palette's signature triad for the field is the
|
||
navy / moss / off-white identity the palette is built on. Recommended bindings (staff-engineer picks the
|
||
exact `--mud-palette-*` vars per mode for the richest spread; the palette is the source either way):
|
||
|
||
- In **light**: navy = `--mud-palette-primary` (`#17283f`), moss = `--mud-palette-secondary` (`#3D7A68`)
|
||
or tertiary (`#429d6a`), and a third anchor for variety — the off-white ground or a deeper navy.
|
||
- In **dark**: green is primary (`#3D7A68`), navy is the ground (`#0D1B2A`), off-white is secondary
|
||
(`#FAFAF8`).
|
||
|
||
The exact triad per mode is a tuning call; the requirement is **three theme-sourced colors, no
|
||
hardcoded hexes.**
|
||
|
||
### 6b. The gradient: A → B, linear, center → outer, with three combined motions
|
||
|
||
The gradient is **always color A → color B, linear, running from the 0 center line outward along the
|
||
waveform** — A at the center/root, B at the outer/extended edge. On top of that static structure, three
|
||
motions combine:
|
||
|
||
**Motion 1 — anchor rotation among X, Y, Z (the gradient rotation speed control).**
|
||
Three theme colors X, Y, Z are in play. Over time, the gradient's two anchors **A and B rotate smoothly
|
||
*among* X, Y, Z** — both the root color A and the extended color B cycle through the three colors. The
|
||
**gradient-rotation-speed control** drives this rotation rate. **The blend must NOT travel through the
|
||
rainbow** — interpolate in **OKLab** (§6c) so the blend stays faithful to the three theme colors with no
|
||
hue drift and no cyan excursion.
|
||
|
||
**Motion 2 — per-bar sinusoidal variation, baked at segment entry (chosen realization: mix-time-keyed).**
|
||
Each bar's A and B vary slightly by a sinusoidal transfer, so the colors change in **"waves" across the
|
||
waveform** rather than one uniform gradient. **Critical implementation requirement:** a segment's colors
|
||
are **chosen when it enters (incoming, at the bottom) and stay FIXED for that segment as the waveform
|
||
scrolls up, until it scrolls out of view.** Colors are **baked per-segment at entry, not recomputed per
|
||
frame.**
|
||
|
||
**Decided realization (Daniel, 2026-06-16): key the per-bar sinusoid to the segment's mix-time, not its
|
||
screen-Y.** Daniel approved this approach explicitly ("whatever works and isn't a clusterfuck to
|
||
maintain"). Because mix-time is fixed for a given segment, the sinusoid becomes a **pure function of
|
||
mix-time** and is therefore **fixed-per-segment by construction** — it travels with the segment as it
|
||
scrolls, with **no explicit per-segment ring buffer to maintain.** This is the chosen approach.
|
||
**Rejected for maintainability:** an explicit ring buffer keyed to scroll position that tracks each
|
||
visible segment's baked A/B colors — it is more moving parts and more state to keep coherent for the
|
||
same visual result. Do not build it; use the mix-time-keyed sinusoid.
|
||
|
||
**Motion 3 — per-bar gradient curve shifts with scroll height.**
|
||
Each bar's gradient *curve* (the A→B mix profile along its own height) shifts as it scrolls up. At the
|
||
bottom a bar is mostly A (e.g. `linear-gradient A 90% → B`), and by the top it is mostly B (`A 10% → B`)
|
||
— so color appears to move outward/inward as the bar scrolls up. This is a function of the segment's
|
||
scroll height (its screen-Y / scroll position), composed on top of the A/B colors that Motion 2 baked
|
||
for that segment.
|
||
|
||
The three motions compose: Motion 1 sets *which* two theme colors A and B are right now (rotating among
|
||
X/Y/Z), Motion 2 perturbs A and B slightly per-segment in fixed-at-entry waves (keyed to mix-time), and
|
||
Motion 3 shifts the A→B blend curve along each bar as it climbs.
|
||
|
||
### 6c. OKLab, not HSL — and why
|
||
|
||
The prior cyan bug came from **HSL** interpolation: the renderer's `mixHsl` blended hue/sat/lum
|
||
independently and `vivify` boosted saturation, which dragged the navy→moss path through saturated
|
||
cyan/teal hue regions (navy's hue sits near blue, moss near green; the short-way hue arc between them
|
||
passes through cyan, and the saturation boost made that excursion vivid). **Interpolate in OKLab
|
||
instead.** OKLab is a perceptually-uniform color space where a straight line between two colors stays
|
||
perceptually faithful — no hue drift, no saturation pumping, no rainbow excursion between the theme
|
||
anchors. Implement OKLab↔linear-sRGB conversion in the shader (well-documented matrices) and `mix()` in
|
||
OKLab. Drop the `mixHsl` / `vivify` / `VIVID_*` machinery entirely.
|
||
|
||
> **Why this is a model fix, not a tuning fix:** no amount of tuning the HSL saturation floor avoids the
|
||
> cyan arc — it is inherent to interpolating hue between blue and green. OKLab removes the failure mode
|
||
> structurally. (Reference: Björn Ottosson's OKLab — the now-standard recommendation for perceptual
|
||
> color interpolation in shaders.)
|
||
|
||
---
|
||
|
||
## 7. The six controls + the inline collapse/expand knob-bar
|
||
|
||
### 7a. The six controls (replacing the four)
|
||
|
||
| # | Control | What it drives | Range | Replaces |
|
||
|---|---------|----------------|-------|----------|
|
||
| 1 | **Waveform scroll speed** | Apparent bottom-to-top scroll rate (decouples scroll from zoom) | normalized, mapped via `MixZoomMapping` or a new scroll-rate map | the old **resolution/zoom** control — *resolution as a standalone control is gone* |
|
||
| 2 | **Color gradient rotation speed** | Motion 1 anchor-rotation rate among X/Y/Z (§6b) | normalized 0→1 → slow→fast cycle | the old **color-shift speed** |
|
||
| 3 | **Lava gravity** | Downward force on the wax (§4d) | normalized 0→1 | new |
|
||
| 4 | **Lava heat** | Energy into the system; 0 = rest-at-bottom, max = many rising/morphing (§4c) | normalized 0→1 | the old **detach**, re-modeled |
|
||
| 5 | **Blob density/size** | Amount of wax — count/size within the 16–32 / 20–100px bands (§4e) | normalized 0→1 | new (the old **bubblyness** is gone; bulge is now physical) |
|
||
| 6 | **Collision strength** | Soft → 100% hard waveform/blob collision (§5c) | normalized 0→1 → soft→hard | new |
|
||
|
||
Note the swap: **resolution/zoom as a standalone control is removed — scroll speed replaces it.** The
|
||
`MIN_VISIBLE_SECONDS` anchor still exists internally for the datum-density framing, but the user-facing
|
||
control is "how fast does it scroll," not "how much do I see." Staff-engineer decides whether scroll
|
||
speed reuses `MixZoomMapping` (treating it as a scroll-rate map) or gets a fresh linear map; either way
|
||
keep the log-feel continuity that made the zoom slider feel good.
|
||
|
||
Defaults are Daniel's to tune on screen (his standing preference — he tunes ranges by hand once it is
|
||
live). Recommended starting points: scroll speed ~mid, rotation ~0.3, gravity ~0.5, heat ~0.3, density
|
||
~0.4, collision ~0.5. These are feel-anchors, not commitments.
|
||
|
||
### 7b. The inline collapse/expand knob-bar (decided — NOT a popover or drawer)
|
||
|
||
**Decision (Daniel, 2026-06-16): the controls are an inline collapse/expand knob-bar, NOT a floating
|
||
popover or a drawer.** The six RadialKnobs live **inline in the controls area** (where the controls sit
|
||
today) as an **`@if`-guarded flex row** that **animates open and closed in place**. It must read as
|
||
**part of the controls collapsing/expanding** — the knob row expanding out from its predecessor — **not**
|
||
a separate surface hanging off the icon.
|
||
|
||
**The mechanism (no MudPopover, no MudDrawer):**
|
||
|
||
- A **bound `bool`** (e.g. `_controlsExpanded`) gates the knob row. The **lava-lamp icon button toggles
|
||
it** — the same `DDIcons.LavaLamp` trigger kept from Phase 10 §7c, now redrawn (§7f), now a
|
||
collapse/expand toggle instead of a popover anchor.
|
||
- When toggled open, the flex row of six knobs **animates open** via **CSS transition** — width / opacity
|
||
/ transform — **expanding out from the icon / its predecessor element**, so it reads as the controls
|
||
growing in place. When toggled closed it collapses back the same way.
|
||
- **Animation intent:** a smooth in-place expansion (expand-from-icon / slide-open flex row), reading as
|
||
one continuous collapse/expand of the controls area — not a panel popping into existence over the page.
|
||
Use the codebase's existing transition vocabulary (the existing controls/transition CSS) so the motion
|
||
feels native, not bolted on.
|
||
- **No floating surface.** There is no overlay, no anchored popover panel, no edge drawer. The knob row is
|
||
a real inline child of the controls area that occupies layout when open and collapses to nothing
|
||
(or to just the icon) when closed.
|
||
|
||
**Why this over the Phase 10 popover.** The popover read as a detached panel hanging off the icon; Daniel
|
||
wants the controls to *be* the controls — collapsing and expanding in place. The inline animated flex row
|
||
keeps the six knobs in the page's flow, makes the open/close a property of the controls themselves, and
|
||
avoids the popover/drawer overlay machinery entirely. (Prior-art touchstone: an inline "expand for
|
||
advanced settings" disclosure row — e.g. a toolbar that grows a secondary row of controls in place —
|
||
rather than a flyout/menu.)
|
||
|
||
**Layout note.** Six knobs in a flex row may wrap on narrow viewports (2×3 / 3×2) — a layout call, but
|
||
all six must remain reachable, and the wrap must still read as part of the inline collapse/expand (the
|
||
whole block grows/shrinks), not as a separate surface.
|
||
|
||
### 7c. State — widen to six properties
|
||
|
||
Widen `MixVisualizerControlState` from four properties to **six**: `ScrollSpeed` (replacing
|
||
`VisibleSeconds` as the user-facing axis, or keep `VisibleSeconds` internally and add a `ScrollSpeed`
|
||
that maps to it — staff-engineer's call), `GradientRotationSpeed` (rename of `ColorShiftSpeed`),
|
||
`LavaGravity`, `LavaHeat` (re-modeled from `Detach`), `BlobDensity`, `CollisionStrength`. Each with a
|
||
`const` default mirrored to the TS tuning anchors (keep the C#↔TS default-sync discipline the existing
|
||
`Default*` consts have). Same scoped-DI persistence model: survives SPA nav within a session, resets on
|
||
fresh load. Same `Changed` event seam — the bridge subscribes and pushes the affected uniform; the
|
||
knob-bar component only mutates state and raises `Changed`. **This is the same architecture as today, just
|
||
six properties instead of four.**
|
||
|
||
The bridge handle gains setters for the new controls (`setScrollSpeed`, `setGradientRotationSpeed`,
|
||
`setLavaGravity`, `setLavaHeat`, `setBlobDensity`, `setCollisionStrength`) — extend, don't redesign,
|
||
mirroring how Phase 10 §2d extended the handle.
|
||
|
||
### 7d. RadialKnob — consumed unchanged, six instead of four
|
||
|
||
`RadialKnob` (`DeepDrftShared.Client/Components/RadialKnob.razor`) is consumed as-is (its API is fixed —
|
||
`Value`/`ValueChanged`/`Min`/`Max`/`Step`/`Label`/`Size`/`Color`/`HoldValue`; no icon slot; `Label` is
|
||
SVG text). Six knobs in the inline bar, each with an adjacent `MudIcon` caption (the no-icon-slot
|
||
constraint from Phase 10 §7e still holds). `HoldValue=false` so they are live. `Step="0.001"` for
|
||
continuous feel. Suggested Material icons per control (staff-engineer picks final glyphs):
|
||
|
||
| Knob | Suggested icon |
|
||
|------|----------------|
|
||
| 1. Scroll speed | `FastForward` / `Speed` |
|
||
| 2. Gradient rotation speed | `Palette` / `Rotate90DegreesCcw` |
|
||
| 3. Lava gravity | `ArrowDownward` / `FilterDrama` |
|
||
| 4. Lava heat | `LocalFireDepartment` / `Whatshot` |
|
||
| 5. Blob density/size | `BubbleChart` / `Grain` |
|
||
| 6. Collision strength | `Adjust` / `Compress` |
|
||
|
||
Six knobs in a row may wrap on narrow viewports (2×3 / 3×2) — a layout call, but all six must remain
|
||
reachable.
|
||
|
||
### 7e. Aesthetic target — match the NowPlaying hero
|
||
|
||
**The knob-bar's color and structure must match the "NowPlaying" section in the hero.** The closest
|
||
existing component to that aesthetic in the codebase is the **Session detail hero**
|
||
(`DeepDrftPublic.Client/Pages/SessionDetail.razor` + `.css`) — the hero-dominant overlay composition: a
|
||
large background image with a darkening gradient shim (`.session-hero-shim`), and overlay rows
|
||
(`.session-hero-top` / `.session-hero-bottom`) carrying title/artist, genre/date chips, and the play
|
||
affordance with translucent surfaces over the image. The structural cues to borrow:
|
||
|
||
- A **translucent dark surface** (the shim aesthetic) so the knob-bar reads as floating glass over the
|
||
visualizer, not an opaque panel.
|
||
- The **overlay-chip / overlay-label typography** (`.session-overlay-label` / `.session-overlay-value`)
|
||
for the knob captions, so the bar's labels match the hero's.
|
||
- `Color.Secondary` accents (the play affordance and the lava-lamp trigger both use `Color.Secondary`) —
|
||
keep the knobs/captions on the same accent so the bar feels of-a-piece with the hero.
|
||
|
||
Staff-engineer studies `SessionDetail.razor[.css]` and matches its surface color + structural rhythm.
|
||
The intent: expand the knob-bar and it looks like it belongs to the same design family as the session
|
||
hero's now-playing overlay, not a generic MudBlazor panel — even though it is an inline collapse/expand
|
||
element, not a floating one (§7b). (If Daniel has a *specific* NowPlaying component in mind other than
|
||
the session hero overlay, confirm — but the session hero is the strongest match in the current tree.)
|
||
|
||
### 7f. Redraw the lava-lamp glyph — classic 1970s silhouette
|
||
|
||
**Decision (Daniel, 2026-06-16): the current `DDIcons.LavaLamp` is rejected ("form is shit, colors are
|
||
shit") and must be redrawn** to the classic 1970s "Lava" lamp silhouette (Daniel supplied a reference
|
||
image). The trigger placement (top-right, across from the back link) is unchanged; only the glyph art
|
||
changes.
|
||
|
||
**Home & authoring convention.** Authored in `DeepDrftShared.Client/Common/DDIcons.cs` as **inner SVG
|
||
markup only — no outer `<svg>` wrapper** (MudBlazor supplies the wrapper). A **24×24 viewBox** coordinate
|
||
space (matches the MudBlazor `viewBox="0 0 24 24"` wrapper and the existing `DDIcons` convention). A
|
||
`public const string` raw-string literal, same as the other icons in the file.
|
||
|
||
**Silhouette (bottom → top) — "offset cones":**
|
||
|
||
1. A **WIDE truncated-cone metal BASE** — widest at the very bottom, tapering *up* to a narrow waist.
|
||
2. A tall tapered **GLASS VESSEL** sitting on the base — **bulbous/rounded at the bottom**, tapering
|
||
**upward** to a **roundedly-POINTED top** (an elongated teardrop / bullet shape — pointy-ish but
|
||
**NOT sharp**).
|
||
3. A small **truncated-cone metal CAP** on top, **mirroring the base**.
|
||
|
||
The read is: cone base + tapered teardrop body + small cone cap. This is the iconic 1970s Lava lamp
|
||
profile — distinct from the current narrow straight-sided vessel, which is rejected.
|
||
|
||
**Colors (on-theme AND faithful to the reference).** The reference shows a silver metal base/cap, blue
|
||
fluid, and green blobs. Map to the app theme:
|
||
|
||
- **Fluid: navy. Blobs: moss.** Navy and moss **are** the theme's blue + green, so this is both faithful
|
||
to the reference and on-theme by construction.
|
||
- **Base + cap: neutral / metallic** (the silver of the reference).
|
||
- The **body silhouette (glass vessel outline) can be `currentColor`** so it tints with context
|
||
(`Color.Secondary`, light/dark) exactly as the existing icons do. The **fluid and blobs carry the
|
||
navy/moss accents** as the icon's color identity.
|
||
|
||
**Keeping the icon theme-aligned (the one source-of-truth wrinkle).** The single source of truth for
|
||
theme colors is `DeepDrftPalettes` (§6a), but an inline SVG `const string` in `DDIcons.cs` **cannot
|
||
resolve `var(--mud-palette-*)`** — a fill on an SVG path inside a raw-string literal must be a literal
|
||
value (or `currentColor`). So the navy/moss fluid/blob stops will need to be **literal hex stops** in the
|
||
SVG. To minimize the duplication-of-truth this creates:
|
||
|
||
- Prefer **`currentColor` for everything that can be** (the body silhouette, ideally the metal base/cap
|
||
via a neutral-tinted variant) so the bulk of the icon themes for free.
|
||
- For the two accent fills that genuinely must be literal (navy fluid, moss blobs), **use the exact
|
||
`DeepDrftPalettes` navy/moss hexes** (e.g. navy `#17283f`, moss `#3D7A68`) and **add a code comment in
|
||
`DDIcons.cs` naming the `DeepDrftPalettes` member each literal mirrors**, so the two literals are
|
||
explicitly traceable to the source and a future palette change has a documented sync point. This is the
|
||
same `currentColor`-where-possible + commented-literal-where-not discipline the existing gas-lamp flame
|
||
icon already uses (its `#FF9800`/`#FFCA28` flame stops are literals on an otherwise-`currentColor`
|
||
lamp). The hard requirement: **no silent second copy of the palette** — `currentColor` first, commented
|
||
literal only where SVG forces it.
|
||
|
||
(The precise path data is staff-engineer's; this fixes the silhouette, the color mapping, the authoring
|
||
convention, and the theme-alignment discipline — not the exact coordinates.)
|
||
|
||
---
|
||
|
||
## 8. Acceptance criteria (observable)
|
||
|
||
**Spatial / layout**
|
||
|
||
1. **Same-plane collision.** The waveform visibly pushes the wax out of its way — blobs part around the
|
||
waveform silhouette rather than overlapping it indiscriminately. The waveform shape is never deformed
|
||
by the fluid (read-only authority preserved).
|
||
2. **Bottom-anchored waveform.** The waveform still rises from the bottom and scrolls bottom-to-top,
|
||
unchanged from 8.K.
|
||
3. **Footer clip.** With the player expanded, no waveform or lava pixel paints over or under the player
|
||
bar. Minimizing the player drops the clip + lava-rest line to match the FAB footer. Changing the
|
||
window size (player-bar breakpoint/height change) re-clips with no reload.
|
||
4. **Noise removed.** No static screen-space noise/frost/dirt layer remains; the screen reads clean.
|
||
|
||
**Lava behavior**
|
||
|
||
5. **Heat 0 = rest + collision-only.** At heat 0 the wax pools at the rest line and does not rise on its
|
||
own; the waveform pushing through it still displaces it (collision always on).
|
||
6. **Heat max = active.** At max heat, many bubbles rise and morph per second — an actively roiling lamp.
|
||
7. **Blobs.** Blobs read as varied organic wax shapes (not N identical circles), in the ~20–100px range,
|
||
merging and splitting via `smin` — no "giant disconnected circles."
|
||
8. **Gravity** changes settling/rise behavior independently of heat.
|
||
9. **Blob density/size** changes how much wax is in the system across its range.
|
||
|
||
**Collision**
|
||
|
||
10. **Two elastic pairs.** Blob↔waveform and blob↔blob both collide elastically.
|
||
11. **Collision strength** sweeps soft (fluid yields and recovers) → hard (rigid wall, no penetration)
|
||
across its range.
|
||
|
||
**Color**
|
||
|
||
12. **Three-color OKLab gradient.** A→B linear from center outward; no cyan, no rainbow excursion — the
|
||
blend stays faithful to the theme colors at all rotation phases.
|
||
13. **Anchor rotation.** A and B rotate among X/Y/Z over time at the gradient-rotation-speed rate;
|
||
dragging the control visibly changes the rotation rate, never frozen at the slow end.
|
||
14. **Per-segment bake (mix-time-keyed).** A segment's colors are fixed when it enters at the bottom and
|
||
travel with it unchanged as it scrolls up and out — colors do not recompute under a stationary
|
||
segment. (Realized via the mix-time-keyed sinusoid, §6b motion 2.)
|
||
15. **Per-bar curve shift.** A bar is mostly A at the bottom and mostly B by the top — color appears to
|
||
move outward as the bar climbs.
|
||
16. **One source of truth.** No hardcoded theme hexes in the visualizer; a `DeepDrftPalettes` edit or a
|
||
dark-mode toggle re-themes the field live with no duplicate to maintain. (The icon's two accent
|
||
literals are the documented, commented exception — §7f.)
|
||
|
||
**Controls / knob-bar**
|
||
|
||
17. **Six controls.** Exactly six RadialKnobs — scroll speed, gradient rotation speed, gravity, heat,
|
||
density/size, collision strength — each captioned with an icon; resolution/zoom as a standalone
|
||
control is gone.
|
||
18. **Inline collapse/expand.** Clicking the lava-lamp icon expands the six-knob flex row **in place**
|
||
(animated open/closed via CSS transition), reading as the controls collapsing/expanding — **not** a
|
||
floating popover or drawer. Clicking again collapses it; dragging a knob does not collapse it.
|
||
19. **NowPlaying aesthetic.** The knob-bar's surface color + structure match the session hero now-playing
|
||
overlay (translucent dark glass, overlay-label typography, `Color.Secondary` accents).
|
||
20. **Persistence + read-only.** All six positions survive SPA nav within a session, reset on fresh
|
||
load; no control and no knob-bar element is a seek/playback surface; the bridge and read-only contract
|
||
are intact.
|
||
|
||
**Icon**
|
||
|
||
21. **Redrawn lava-lamp glyph.** The trigger shows the classic 1970s silhouette — wide truncated-cone
|
||
base, bulbous→roundedly-pointed teardrop glass body, small cone cap — with navy fluid + moss blobs
|
||
and a neutral/metallic base+cap; body tints via `currentColor`; renders cleanly at ~24px.
|
||
|
||
**Performance**
|
||
|
||
22. **60 FPS** on a mid-range desktop with heat, density, and collision at non-trivial values
|
||
simultaneously; graceful degrade (drop internal resolution before frames) on weaker/mobile devices.
|
||
|
||
---
|
||
|
||
## 9. Suggested phasing / waves (R1–R4)
|
||
|
||
A **physics-and-collision first, color second, knob-bar third** sequence — the lava is the real work and
|
||
the riskiest, so prove it before the gradient and the UI rework. **These are reframe waves R1–R4, folded
|
||
under the open Phase 10 and distinct from its landed Waves 1–4.**
|
||
|
||
### Wave R1 — De-noise + footer clip + icon redraw (cheap, unblocks a clean canvas)
|
||
Remove the static noise/frost layer (§3), implement the dynamic footer-height clip + lava-rest line
|
||
(§2c), and **redraw the `DDIcons.LavaLamp` glyph** (§7f — independent, can land anytime, grouped here as
|
||
cheap polish). Small, independent, and gives a clean substrate to build the lava on. **Acceptance:** §8
|
||
#3, #4, #21.
|
||
|
||
### Wave R2 — The wax-blob physics + collision (the load-bearing step)
|
||
Stand up the CPU physics step (16–32 blobs: position/velocity/temperature/radius), the per-frame uniform
|
||
upload, the `smin` metaball render, the heat/gravity/density mapping, and the 2D collision model (both
|
||
pairs, the soft↔hard blend). This is where the architecture is proven; it replaces the §4-effect GLSL.
|
||
**Acceptance:** §8 #1, #5–#11, #22 (at this wave's workload). Controls can be temporary sliders/debug
|
||
knobs here — the real inline knob-bar is Wave R4.
|
||
|
||
### Wave R3 — The OKLab three-color gradient (the three motions)
|
||
Replace the HSL `mixHsl`/`vivify` color with OKLab interpolation; implement the three motions (anchor
|
||
rotation among X/Y/Z, per-segment baked sinusoidal variation **keyed to mix-time**, per-bar curve shift),
|
||
sourced from `DeepDrftPalettes` with no hardcoded duplicates. **Acceptance:** §8 #12–#16. The
|
||
per-segment-bake requirement (§6b motion 2) uses the mix-time-keyed realization so it travels with the
|
||
segment by construction (decided — §6b).
|
||
|
||
### Wave R4 — Six controls + the NowPlaying-styled inline knob-bar
|
||
Widen `MixVisualizerControlState` to six properties; replace the four-knob popover with the six-knob
|
||
**inline collapse/expand knob-bar** (§7b — `@if`-guarded animated flex row, lava-lamp icon toggles it, no
|
||
popover/drawer); style it to the session-hero aesthetic; extend the bridge handle with the new setters.
|
||
**Acceptance:** §8 #17–#20.
|
||
|
||
**Dependency shape:** `Wave R1 → Wave R2 → (Wave R3 ‖ Wave R4)`. Wave R1 is a quick unblock (and folds in
|
||
the independent icon redraw). Wave R2 is the prerequisite for everything visual. Waves R3 (color) and R4
|
||
(controls/knob-bar) both depend on Wave R2 but are independent of each other — the gradient can land
|
||
before the knob-bar, or vice versa, and each control's effect is independently tunable. Daniel tunes
|
||
ranges/transfer-functions by hand once on screen throughout (his standing preference).
|
||
|
||
---
|
||
|
||
## 10. Open items
|
||
|
||
Tuning knobs only — the controls-UI fork and the per-segment-storage fork are both now **decided** (§7b
|
||
inline knob-bar; §6b mix-time-keyed). None block starting Wave R1.
|
||
|
||
- **§4c, §5d — transfer functions:** heat 0..1 → rise/morph intensity; collision 0..1 → soft↔hard blend
|
||
shape; restitution coefficients; penetration-penalty curve. All staff-engineer tuning tasks with the
|
||
endpoints fixed here.
|
||
- **§4g — blob count band** within 16–32 and the density-control mapping to count vs. radius.
|
||
- **§6a — exact X/Y/Z theme-var bindings** per light/dark for the richest spread (the palette is the
|
||
source either way).
|
||
- **§7a — scroll-speed mapping:** reuse `MixZoomMapping` as a scroll-rate map vs. a fresh linear map.
|
||
- **§7e — NowPlaying source:** confirm the session hero overlay is the intended "NowPlaying" aesthetic
|
||
(strongest match in the tree) or point to a different component.
|
||
- **§7f — icon accent literals:** the two literal navy/moss stops the SVG forces (commented to their
|
||
`DeepDrftPalettes` source) — exact hexes are staff-engineer's to pull from the palette.
|
||
- **Defaults** for all six controls — Daniel tunes on screen.
|