docs(plan): add Phase 12 Mix Visualizer Lava Reframe spec; supersede Phase 10 effects/controls
This commit is contained in:
@@ -234,6 +234,23 @@ Sequenced as **seven waves**; the critical path is `11.A → 11.B → 11.C`, wit
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Phase 12 — Mix Visualizer Lava Reframe
|
||||||
|
|
||||||
|
A **major reframe of the Mix visualizer's effects, controls, and color model**, building on the landed Phase 10 WebGL2 renderer infrastructure (pipeline, datum texture, playhead interp, bridge, widened body, lava-lamp trigger) but **replacing what it paints**. Daniel tested the Phase 10 effects end-to-end and rejected the visual result: the lava read as "giant disconnected circles," the colors drifted to cyan (an HSL saturation-boost artifact), and the waveform and lava read as two unrelated things sharing a canvas. The diagnosis (staff-engineer research pass) is that the rejected look is **structural to the effect approach, not a tuning miss**.
|
||||||
|
|
||||||
|
**This supersedes the Phase 10 effects/controls/color design** — `product-notes/mix-visualizer-webgl-renderer.md` §4 (effects) and §7 (popover-controls) are marked superseded with a pointer to the new spec. The renderer *infrastructure* carries forward unchanged.
|
||||||
|
|
||||||
|
**The three reframes:**
|
||||||
|
- **Lava → CPU-physics wax blobs.** Keep the single-pass WebGL2 fragment renderer; add a small CPU-side per-frame physics step modeling ~16–32 Lagrangian "wax blobs" (position/velocity/temperature/radius) uploaded as uniforms and blended with `smin` SDF metaballs. The waveform and lava share **the same plane WITH real 2D elastic collision** (blob↔waveform-boundary + blob↔blob) — the waveform pushes the fluid out of its way (read-only authority preserved; the fluid never deforms the waveform). At heat 0 the wax rests at the bottom and only collision moves it (collision always on, independent of heat); at heat max many bubbles rise/morph per second. **Rejected: a full ping-pong FBO Navier-Stokes fluid sim** — a lava lamp is high-viscosity/low-turbulence, the opposite regime; large rewrite for unwanted realism. Deliberate later upgrade only.
|
||||||
|
- **Color → three-color OKLab gradient with three motions.** One source of truth (`DeepDrftPalettes`), no hardcoded hexes. Always A→B linear from the center line outward. Three combined motions: (1) anchors A/B **rotate among three theme colors X/Y/Z** at the rotation-speed control's rate — **OKLab interpolation, never through the rainbow** (the cyan fix is structural, not a tuning dial); (2) per-bar sinusoidal variation **baked at segment entry and fixed as the segment scrolls** (implies per-segment color state — cleanest realization: key the sinusoid to mix-time so it travels by construction); (3) per-bar gradient curve shifts with scroll height (mostly A at bottom → mostly B at top). The static noise/frost texture is **removed** (Daniel: makes the screen look dirty).
|
||||||
|
- **Controls → six knobs in a flyout.** Replaces the four: (1) waveform scroll speed [replaces resolution/zoom as a standalone control], (2) gradient rotation speed, (3) lava gravity, (4) lava heat, (5) blob density/size, (6) collision strength (soft→hard). **NOT a popover — an extended flyout menu bar** (MudBlazor survey: recommend a horizontal `MudPopover` styled as a knob bar, or `MudDrawer Anchor="Bottom"` if Daniel wants the edge-slide motion — the one Daniel call worth making up front). The six `RadialKnob`s live in the flyout, styled to **match the NowPlaying hero aesthetic** (the session-detail hero overlay — translucent dark glass, overlay-label typography, `Color.Secondary`). Also: overflow-clip the visualizer to the **dynamic footer height** (the player bar changes height minimized/expanded) so visuals stop cleanly above it; the clip line is also the lava rest line.
|
||||||
|
|
||||||
|
Heat→intensity and collision soft↔hard transfer functions are **staff-engineer tuning tasks** (endpoints fixed in the spec, formulas not). Full design, the wax-blob model, the collision model, the three-motion color model, the flyout survey, observable acceptance criteria, and phasing: `product-notes/phase-12-mix-visualizer-lava-reframe.md`.
|
||||||
|
|
||||||
|
**Sequenced as four waves.** `Wave 1 → Wave 2 → (Wave 3 ‖ Wave 4)`. Wave 1 (de-noise + dynamic footer clip) is a cheap unblock for a clean substrate. Wave 2 (wax-blob physics + 2D collision) is the load-bearing prerequisite — prove the lava before the color and the UI. Wave 3 (OKLab three-color gradient, the three motions) and Wave 4 (six controls + NowPlaying-styled flyout + widened state to six properties + extended bridge handle) both depend on Wave 2 but are independent of each other. **Open Daniel call:** flyout primitive (popover-flyout vs. bottom drawer — spec §7b/§10).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Working with this file
|
## Working with this file
|
||||||
|
|
||||||
- **Add items by extending an existing phase first**; only create a new phase when the addition genuinely doesn't fit any of 1–5. Phase numbers are organisational, not sequencing.
|
- **Add items by extending an existing phase first**; only create a new phase when the addition genuinely doesn't fit any of 1–5. Phase numbers are organisational, not sequencing.
|
||||||
|
|||||||
@@ -214,6 +214,17 @@ contract on both sides).
|
|||||||
|
|
||||||
## 4. The four visual effects
|
## 4. The four visual effects
|
||||||
|
|
||||||
|
> **SUPERSEDED (2026-06-16) by `product-notes/phase-12-mix-visualizer-lava-reframe.md`.** Daniel tested
|
||||||
|
> the landed effects end-to-end and rejected the visual result — the lava read as "giant disconnected
|
||||||
|
> circles," the color drifted to cyan (an HSL saturation-boost artifact), and the waveform and lava read
|
||||||
|
> as two unrelated things. The reframe replaces this entire effects layer: the per-bar bulge + detach
|
||||||
|
> blobs become a **CPU-physics wax-blob lava with real 2D collision** (the waveform pushes the fluid out
|
||||||
|
> of its way), the HSL navy↔moss treatment becomes a **three-color OKLab gradient with three combined
|
||||||
|
> motions**, the separate "glass" effect folds into the blob shading, and the static noise/frost layer is
|
||||||
|
> **removed**. The renderer *infrastructure* (pipeline, datum texture, playhead interp, bridge) is reused;
|
||||||
|
> the *art* below is replaced. See the new spec §3–§6. The four-effect text below is retained as the
|
||||||
|
> record of what the rejected Wave 3 shipped.
|
||||||
|
|
||||||
Described as **intended look + shader-side approach in conceptual terms**. The exact GLSL is
|
Described as **intended look + shader-side approach in conceptual terms**. The exact GLSL is
|
||||||
staff-engineer's. Each effect is a continuous function of its control value, so the whole range from
|
staff-engineer's. Each effect is a continuous function of its control value, so the whole range from
|
||||||
"off" to "maxed" is reachable by dragging one slider.
|
"off" to "maxed" is reachable by dragging one slider.
|
||||||
@@ -422,6 +433,16 @@ hand).
|
|||||||
|
|
||||||
## 7. Wave 4 — Detail-page polish + controls rework (presentation only)
|
## 7. Wave 4 — Detail-page polish + controls rework (presentation only)
|
||||||
|
|
||||||
|
> **PARTIALLY SUPERSEDED (2026-06-16) by `product-notes/phase-12-mix-visualizer-lava-reframe.md`.**
|
||||||
|
> **Kept:** the lava-lamp icon-button trigger top-right of the body across from the back link (§7c, §7f
|
||||||
|
> — `DDIcons.LavaLamp`, landed) and the widened Mix body (`MudContainer MaxWidth="Large"`, §7g, landed).
|
||||||
|
> **Superseded:** the four-knob **popover** becomes a six-knob **flyout** (the reframe adds two controls —
|
||||||
|
> lava gravity, blob density/size, collision strength — and drops/recasts others: resolution/zoom is
|
||||||
|
> removed in favor of scroll speed; bubblyness/detach are replaced by the physical lava model), and the
|
||||||
|
> popover primitive is reconsidered (`MudDrawer` vs. popover-as-flyout — see the new spec §7b). See the
|
||||||
|
> new spec §7 for the six controls, the flyout survey, and the NowPlaying-hero aesthetic target. The §7
|
||||||
|
> text below is retained as the record of what the four-knob popover shipped.
|
||||||
|
|
||||||
Status: **design-complete, implementation-ready.** Added 2026-06-15. **Depends on Wave 3 being merged**
|
Status: **design-complete, implementation-ready.** Added 2026-06-15. **Depends on Wave 3 being merged**
|
||||||
(the knobs in this wave drive the four effects that Wave 3 makes real). **This wave supersedes the §3
|
(the knobs in this wave drive the four effects that Wave 3 makes real). **This wave supersedes the §3
|
||||||
always-visible controls-row design** — the row moves into a popover and the four MudSliders become four
|
always-visible controls-row design** — the row moves into a popover and the four MudSliders become four
|
||||||
|
|||||||
@@ -0,0 +1,657 @@
|
|||||||
|
# Mix Visualizer — Lava Reframe (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**. It
|
||||||
|
builds on the landed Phase 10 WebGL2 renderer (Waves 1–4: 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.
|
||||||
|
|
||||||
|
**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 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
|
||||||
|
flyout, and the popover primitive is reconsidered in §7b).
|
||||||
|
|
||||||
|
What carries forward unchanged from Phase 10 (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**).
|
||||||
|
|
||||||
|
Cross-references (read these before implementing):
|
||||||
|
|
||||||
|
- `product-notes/mix-visualizer-webgl-renderer.md` — the Phase 10 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 (§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 popover becomes the flyout (§7).
|
||||||
|
- `DeepDrftPublic.Client/Pages/SessionDetail.razor[.css]` — the **NowPlaying / hero aesthetic** the
|
||||||
|
flyout 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).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 flyout** styled to match the hero NowPlaying
|
||||||
|
aesthetic. Remove the static noise texture that makes the screen look dirty.
|
||||||
|
|
||||||
|
**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 **flyout** (replacing the popover) styled to the NowPlaying hero aesthetic (§7).
|
||||||
|
- **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 | Replaced by this reframe |
|
||||||
|
|-------|----------------------|--------------------------|
|
||||||
|
| 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 a flyout (§7) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.**
|
||||||
|
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.** This implies **per-segment color state tied to scroll position** — the renderer must track, for
|
||||||
|
each visible segment, the A/B colors assigned when it entered, and carry them with the segment as it
|
||||||
|
scrolls. (Flagged as a notable implementation requirement: this is per-segment state, not a stateless
|
||||||
|
per-fragment function. Staff-engineer designs the storage — likely a ring buffer keyed to scroll
|
||||||
|
position, or baking the per-segment phase into the datum-time coordinate so the sinusoid is a pure
|
||||||
|
function of mix-time and therefore *automatically* travels with the segment. The mix-time approach is the
|
||||||
|
cleaner realization: if the per-bar sinusoid is keyed to the segment's mix-time rather than its current
|
||||||
|
screen-Y, it is fixed-per-segment *by construction* and scrolls correctly with no explicit buffer.)
|
||||||
|
|
||||||
|
**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, 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 flyout
|
||||||
|
|
||||||
|
### 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. Flyout, not popover — survey of MudBlazor options
|
||||||
|
|
||||||
|
**Reframe from Phase 10 §7:** the controls live in an **extended flyout menu bar**, not a popover.
|
||||||
|
Clicking the lava-lamp icon makes the **RadialKnobs fly out** for editing.
|
||||||
|
|
||||||
|
MudBlazor options surveyed for "click an icon, knobs fly out into an editing strip":
|
||||||
|
|
||||||
|
| Option | Fit | Verdict |
|
||||||
|
|--------|-----|---------|
|
||||||
|
| **`MudDrawer`** (`Anchor="Right"`/`Bottom"`, `Variant="Temporary"` or `"Mini"`) | A real drawer that slides in from an edge; `Open`-bound, overlay-dismissable, themeable. The "Mini" variant expands/collapses in place — close to a flyout bar. | **Recommended for an edge-anchored flyout strip.** A right or bottom temporary drawer reads as "the lava-lamp panel slid in," matches the "extended menu bar" language, and gives the six knobs room in a row/column. |
|
||||||
|
| **`MudPopover` as a flyout** (the Phase 10 idiom, widened) | Anchored floating panel of arbitrary content; already in the codebase (`SharePopover`). Can be styled as a horizontal bar dropping from the icon. | **Acceptable, lighter-weight.** Closest to what Phase 10 shipped; if the flyout should *hang off the icon* rather than slide from a screen edge, a wide horizontal `MudPopover` styled as a knob bar is the smallest change. |
|
||||||
|
| **`MudMenu`** | Built for actionable item lists, not a custom drag-interaction knob row. | **Rejected** — fights the knob drag/click model (same reasoning as Phase 10 §7d). |
|
||||||
|
| **A bespoke CSS expanding panel** (no MudBlazor primitive) | Full control of the "menu bar extends" animation; an absolutely-positioned strip that animates width/opacity from the icon. | **Fallback** if neither drawer nor popover gives the exact "menu bar flies out" motion Daniel wants. More CSS to own. |
|
||||||
|
|
||||||
|
**Recommendation:** start with a **horizontal `MudPopover` styled as a flyout knob bar** anchored to the
|
||||||
|
lava-lamp icon (smallest delta from the landed §7 popover, keeps the icon-button trigger and the
|
||||||
|
outside-click/overlay idiom already wired in `MixDetail.razor`), **unless** Daniel wants the controls to
|
||||||
|
slide from a screen edge — in which case use a **`MudDrawer`** (`Anchor="Bottom"` reads most like an
|
||||||
|
"extended menu bar"). This is a genuine fork on the desired *motion*; both are cheap. Surfaced as the one
|
||||||
|
open question worth a Daniel call (§9).
|
||||||
|
|
||||||
|
Either way: the six RadialKnobs live in the flyout, the lava-lamp icon button is the trigger (kept from
|
||||||
|
§7c — `DDIcons.LavaLamp`, top-right of the body across from the back link), the flyout stays open while
|
||||||
|
dragging a knob (the knob's global mouse-capture overlay must not be read as an outside-click — verify;
|
||||||
|
gate dismiss-on-outside-click off mid-drag if needed, as Phase 10 §7d already noted), and no flyout
|
||||||
|
element is a seek 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
|
||||||
|
flyout 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 flyout, 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 flyout'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 flyout 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 flyout'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 flyout feels of-a-piece with the hero.
|
||||||
|
|
||||||
|
Staff-engineer studies `SessionDetail.razor[.css]` and matches its surface color + structural rhythm.
|
||||||
|
The intent: open the flyout and it looks like it belongs to the same design family as the session hero's
|
||||||
|
now-playing overlay, not a generic MudBlazor panel. (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.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.** 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.
|
||||||
|
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.
|
||||||
|
|
||||||
|
**Controls / flyout**
|
||||||
|
|
||||||
|
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. **Flyout.** Clicking the lava-lamp icon flies the knobs out (drawer or popover-flyout per the §7b
|
||||||
|
decision); clicking outside closes; dragging a knob does not close it.
|
||||||
|
19. **NowPlaying aesthetic.** The flyout'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 flyout element is a seek/playback surface; the bridge and read-only contract
|
||||||
|
are intact.
|
||||||
|
|
||||||
|
**Performance**
|
||||||
|
|
||||||
|
21. **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
|
||||||
|
|
||||||
|
A **physics-and-collision first, color second, flyout third** sequence — the lava is the real work and
|
||||||
|
the riskiest, so prove it before the gradient and the UI rework.
|
||||||
|
|
||||||
|
### Wave 1 — De-noise + footer clip (cheap, unblocks a clean canvas)
|
||||||
|
Remove the static noise/frost layer (§3) and implement the dynamic footer-height clip + lava-rest line
|
||||||
|
(§2c). Small, independent, and gives a clean substrate to build the lava on. **Acceptance:** §8 #3, #4.
|
||||||
|
|
||||||
|
### Wave 2 — 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, #21 (at this wave's workload). Controls can be temporary sliders/debug
|
||||||
|
knobs here — the real flyout is Wave 4.
|
||||||
|
|
||||||
|
### Wave 3 — 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, per-bar curve shift), sourced from
|
||||||
|
`DeepDrftPalettes` with no hardcoded duplicates. **Acceptance:** §8 #12–#16. The per-segment-bake
|
||||||
|
requirement (§6b motion 2) is the subtle part — prefer the mix-time-keyed realization so it travels with
|
||||||
|
the segment by construction.
|
||||||
|
|
||||||
|
### Wave 4 — Six controls + the NowPlaying-styled flyout
|
||||||
|
Widen `MixVisualizerControlState` to six properties; replace the four-knob popover with the six-knob
|
||||||
|
flyout (drawer or popover-flyout per §7b); style it to the session-hero aesthetic; extend the bridge
|
||||||
|
handle with the new setters. **Acceptance:** §8 #17–#20.
|
||||||
|
|
||||||
|
**Dependency shape:** `Wave 1 → Wave 2 → (Wave 3 ‖ Wave 4)`. Wave 1 is a quick unblock. Wave 2 is the
|
||||||
|
prerequisite for everything visual. Waves 3 (color) and 4 (controls/flyout) both depend on Wave 2 but are
|
||||||
|
independent of each other — the gradient can land before the flyout, 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 and one genuine fork. None block starting Wave 1.
|
||||||
|
|
||||||
|
- **§7b — flyout primitive (the one Daniel call worth making up front):** horizontal `MudPopover`
|
||||||
|
styled as a knob bar (smallest delta, hangs off the icon) **vs.** `MudDrawer Anchor="Bottom"` (slides
|
||||||
|
from a screen edge, reads most like an "extended menu bar"). Both cheap; the choice is about the
|
||||||
|
*motion* Daniel wants. Recommend popover-flyout unless he wants the edge-slide.
|
||||||
|
- **§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).
|
||||||
|
- **§6b motion 2 — per-segment color storage** (ring buffer vs. mix-time-keyed sinusoid — recommend
|
||||||
|
mix-time-keyed so it travels by construction).
|
||||||
|
- **§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.
|
||||||
|
- **Defaults** for all six controls — Daniel tunes on screen.
|
||||||
Reference in New Issue
Block a user