feat(visualizer): Phase 15 control-deck rework

Centered tinted MudOverlay (NowPlayingCard chrome) replaces the anchored popover; eight dials become a deterministic three-row LAVA/WAVE layout; lava + waveform lamp toggles drive a genuine per-subsystem draw-skip; scroll/zoom becomes a slider; playful tooltips; green=interactive/light=static.
This commit is contained in:
daniel-c-harvey
2026-06-17 14:28:15 -04:00
parent fe481d0417
commit dd4f8ddded
8 changed files with 465 additions and 184 deletions
@@ -1,56 +1,56 @@
@namespace DeepDrftPublic.Client.Controls
@using DeepDrftShared.Client.Common
@* The single controls affordance, placed by an icon anywhere (Phase 12 §3d-revised). Closed state is
just the lava-lamp icon; clicking it floats the eight-knob WaveformVisualizerControls panel over the
surface. One panel, one popover host, reused on every host (Mix, Cut, Session, NowPlaying card) — the
SOLID seam: variance is the per-host anchor (§8e), never a forked popover.
@* The single controls affordance, placed by an icon anywhere (Phase 12 §3d-revised, re-primitived
Phase 15 §4). Closed state is just the lava-lamp icon; clicking it floats the control panel as a
SCREEN-CENTERED, tinted MODAL over the whole surface. One panel, one host, reused on every host (Mix,
Cut, Session, NowPlaying) — the SOLID seam.
Anchoring follows the SharePopover precedent: Fixed so the panel reads the trigger's bounding rect
rather than fighting CSS container tricks. AnchorOrigin/TransformOrigin are per-host
parameters (§8e) defaulted to bottom-right open-down — the cleanest case (Mix's TopRightAction corner);
tight hosts (the NowPlaying card) override to open away from the card body.
PRIMITIVE (Phase 15 §4): a centered MudOverlay, NOT an anchored MudPopover. The panel must read as
screen-centered regardless of where the lava-lamp icon sits (Mix corner, Cut/Session ambient,
NowPlaying corner). An anchored popover positions off the trigger's bounding rect — the wrong model
for "centered on the screen." So the icon is just an opener; the overlay hosts the panel in its centre
(the overlay is a full-viewport flex container — its content is centered by .waveform-visualizer-control-
overlay in the GLOBAL sheet, since the overlay portals out of this subtree). DarkBackground gives the
mild modal tint (alpha from the single --deepdrft-modal-scrim-alpha token, §10.5). There is therefore
no AnchorOrigin/TransformOrigin: a centered modal has no anchor (Phase 15 drops those parameters).
The popover owns NO control state and NO JS interop. The hosted WaveformVisualizerControls panel mutates
the shared WaveformVisualizerControlState and raises Changed; the visualizer bridge subscribes. This host
only toggles open/closed and places the panel — it stays purely presentational. *@
KNOB-DRAG SAFETY (Phase 15 §4, highest-risk detail): RadialKnob mounts its own full-viewport
position:fixed; z-index:9999 mouse-capture div WHILE dragging (RadialKnob.razor lines 59). That capture
div sits ABOVE the overlay scrim, so a knob drag's pointer-up lands on the capture div, never the scrim
— the overlay's OnClick does not fire mid-drag, so releasing the mouse outside the panel does NOT close
the modal. AutoClose is left OFF (the default) and dismissal is via the explicit scrim OnClick only,
carrying the SharePopover idiom forward under the new primitive. The panel stops click propagation so a
click INSIDE it never bubbles to the scrim's close handler.
@* Backdrop dismissal mirrors SharePopover: a viewport overlay closes on outside click. AutoClose stays
off so a knob drag (which can land pointer-up outside the panel's DOM subtree) does not dismiss. *@
<MudOverlay Visible="@_open" OnClick="@Close" />
The host owns NO control state and NO JS interop. The hosted WaveformVisualizerControls panel mutates
the shared WaveformVisualizerControlState and raises Changed; the visualizer bridge subscribes. This
host only toggles open/closed and centers the panel — it stays purely presentational. *@
@* Activator and popover share this wrapper so MudPopover anchors off the trigger's bounding rect. *@
<div>
<MudTooltip Text="Visualizer settings">
<MudIconButton Icon="@(_open ? DDIcons.LavaLampFilled : DDIcons.LavaLamp)"
Size="@IconSize"
Color="Color.Secondary"
Disabled="@(!RendererInfo.IsInteractive)"
OnClick="@Toggle"
aria-label="Visualizer settings"
aria-expanded="@_open" />
</MudTooltip>
<MudTooltip Text="Visualizer settings">
<MudIconButton Icon="@(_open ? DDIcons.LavaLampFilled : DDIcons.LavaLamp)"
Size="@IconSize"
Color="Color.Secondary"
Disabled="@(!RendererInfo.IsInteractive)"
OnClick="@Toggle"
aria-label="Visualizer settings"
aria-expanded="@_open" />
</MudTooltip>
<MudPopover Open="@_open"
Fixed="true"
AnchorOrigin="@AnchorOrigin"
TransformOrigin="@TransformOrigin"
Class="waveform-visualizer-control-popover">
@* The tinted modal scrim that also HOLDS the panel. DarkBackground = the mild tint; OnClick on the scrim
dismisses (knob-drag-safe, see header). The panel is the overlay's centered child; it stops click
propagation so an inside click is not a dismissal. Modal so focus/scroll stay on the panel. *@
<MudOverlay Visible="@_open"
DarkBackground="true"
Modal="true"
OnClick="@Close"
Class="waveform-visualizer-control-overlay">
<div class="waveform-visualizer-control-modal" @onclick:stopPropagation="true">
<WaveformVisualizerControls PanelChrome="true" />
</MudPopover>
</div>
</div>
</MudOverlay>
@code {
/// <summary>
/// Where the panel anchors relative to the trigger icon (§8e). Defaults to opening down-from the
/// icon's bottom-right — fits Mix's top-right corner and the ambient hosts. Tight hosts (the
/// NowPlaying card) override to open away from the card body.
/// </summary>
[Parameter] public Origin AnchorOrigin { get; set; } = Origin.BottomRight;
/// <summary>The panel's own corner that pins to <see cref="AnchorOrigin"/> (§8e).</summary>
[Parameter] public Origin TransformOrigin { get; set; } = Origin.TopRight;
/// <summary>Trigger-icon size. Defaults Large to match the Phase 10 Mix lava-lamp button.</summary>
[Parameter] public Size IconSize { get; set; } = Size.Large;