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:
@@ -1,139 +1,221 @@
|
||||
@namespace DeepDrftPublic.Client.Controls
|
||||
@using DeepDrftShared.Client.Common
|
||||
@using DeepDrftPublic.Client.Services
|
||||
@inject WaveformVisualizerControlState ControlState
|
||||
|
||||
@* The waveform visualizer control PANEL (Phase 12 §3d-revised). EIGHT continuous RadialKnobs — scroll
|
||||
speed, gradient rotation speed, lava gravity, lava heat, fluid amount, fluid viscosity, collision
|
||||
strength, waveform width — each its own dedicated control with a Material-icon caption. The single
|
||||
"bubbles" knob is split into fluid-amount + fluid-viscosity (Phase 10 §5).
|
||||
@* The waveform visualizer control PANEL (Phase 12 §3d-revised → Phase 15 re-layout). The control deck for
|
||||
the lava-lamp visualizer: a deterministic THREE-ROW, sectioned layout that encodes what the visualizer
|
||||
composes — a LAVA field and a WAVEFORM ribbon, each independently toggleable (Phase 15 §3):
|
||||
|
||||
This component is the PANEL CONTENT hosted inside WaveformVisualizerControlPopover. It no longer rides
|
||||
an inline TopRowCenter bar; it lays out as a wrapped grid sized for a popover, styled to the NowPlaying
|
||||
Hero look (§3g — dark-navy ground, green-accent knobs, light icons) from the deepdrft-* tokens.
|
||||
Row 1 (MODE, always): lava lamp-toggle, waveform lamp-toggle, [collisions knob — only when BOTH on],
|
||||
then the color knob pinned far-right (applies to the whole field, so always visible).
|
||||
Row 2 (LAVA, only when lava on): "LAVA:" label + gravity / heat / fluid-amount / fluid-viscosity.
|
||||
Row 3 (WAVE, only when waveform on): "WAVE:" label + scroll SLIDER + width knob pinned far-right.
|
||||
|
||||
Because MudPopover PORTALS its content out of this component's DOM subtree, Blazor CSS isolation does
|
||||
not reach the rendered panel — so panel chrome lives in the GLOBAL deepdrft-styles.css
|
||||
(.waveform-visualizer-control-panel*), not in the scoped .razor.css. The scoped .razor.css carries only
|
||||
the inline-bar fallback (the .mix-visualizer-controls-bar reserved-height row) Mix's existing
|
||||
TopRowCenter mount still uses, which is NOT portaled and so resolves under isolation.
|
||||
The two lamp toggles are iconographic (lit LavaLampFilled / unlit LavaLamp glyph), green because they
|
||||
are INTERACTIVE (the §5 colour principle: green = interactive, light = non-interactive). The eight
|
||||
continuous dials are unchanged in tuning; the one widget-type change is scroll/zoom → a MudSlider (§8,
|
||||
bound to ScrollSpeed alone). None of these is a seek surface (read-only contract §D).
|
||||
|
||||
Visibility: the popover host always shows the panel when open (Visible defaults true). Mix's legacy
|
||||
inline mount still feeds its lava-lamp toggle into Visible (Phase 10 §4): the knobs @if-gate while the
|
||||
container holds a reserved min-height so content below never pops on toggle.
|
||||
This is the PANEL CONTENT hosted inside WaveformVisualizerControlPopover, now a screen-centered tinted
|
||||
MudOverlay (Phase 15 §4). Because the overlay PORTALS its content out of this component's DOM subtree,
|
||||
Blazor CSS isolation does not reach the rendered panel — so panel chrome AND the row/section layout live
|
||||
in the GLOBAL deepdrft-styles.css (.waveform-visualizer-control-panel*), not the scoped .razor.css. The
|
||||
scoped .razor.css carries only the legacy inline-bar fallback (Mix's old non-portaled mount), which may
|
||||
now be dead post-Phase-12 but is left in place — flagged, not cut (out of Phase 15 scope).
|
||||
|
||||
COLOUR PRINCIPLE (§5): the lamp toggles + knob arcs/pointers + the slider track/thumb are green-accent
|
||||
(interactive); the "LAVA:"/"WAVE:" section labels and the knob caption icons are LIGHT (static). All
|
||||
colours are token-sourced (deepdrft-tokens.css) — no hardcoded hex.
|
||||
|
||||
It owns NO JS interop: it mutates the shared, session-scoped WaveformVisualizerControlState and raises
|
||||
its Changed event. The visualizer bridge (WaveformVisualizer) subscribes to that event and pushes the
|
||||
affected dial to the WebGL module. That keeps the JS module handle single-owned by the bridge and this
|
||||
component purely presentational. None of these is a seek surface (read-only contract §D).
|
||||
|
||||
RadialKnob has no icon slot (its Label renders as SVG text) and no aria attribute-capture, so each
|
||||
control's Material icon rides beside its knob as an adjacent MudIcon caption and the accessible name
|
||||
rides on the wrapping group div (§7d). HoldValue stays false so the knobs are live — ValueChanged fires
|
||||
continuously during drag, preserving the Changed/NotifyChanged seam. *@
|
||||
its Changed event. The visualizer bridge (WaveformVisualizer) subscribes and pushes the affected dial /
|
||||
subsystem-enable to the WebGL module. RadialKnob has no icon slot (its Label renders as SVG text) and no
|
||||
aria capture, so each control's Material icon rides beside its knob as a caption and the accessible name
|
||||
rides on the wrapping group div (§7); the playful MudTooltip rides alongside for sighted hover. *@
|
||||
|
||||
<div class="@($"{_panelChromeClass} mix-visualizer-controls-bar".TrimStart())">
|
||||
|
||||
@if (Visible)
|
||||
{
|
||||
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Waveform scroll speed">
|
||||
<RadialKnob Value="@ControlState.ScrollSpeed"
|
||||
ValueChanged="@OnScrollSpeedChanged"
|
||||
Min="0" Max="1" Step="0.001"
|
||||
Size="64"
|
||||
Color="Color.Primary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.Speed" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
||||
@* ── Row 1 — MODE (always visible). Toggles + collisions group left; color pinned right. ── *@
|
||||
<div class="wvc-row wvc-row-mode">
|
||||
<div class="wvc-row-left">
|
||||
|
||||
<MudTooltip Text="Light the lamp — or let it go cold.">
|
||||
<div class="wvc-toggle" role="group" aria-label="Lava field on or off">
|
||||
<MudIconButton Icon="@(ControlState.LavaEnabled ? DDIcons.LavaLampFilled : DDIcons.LavaLamp)"
|
||||
Color="Color.Primary"
|
||||
OnClick="@ToggleLava"
|
||||
aria-label="Lava field on or off"
|
||||
aria-pressed="@ControlState.LavaEnabled" />
|
||||
</div>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="Show the sound, or hide the ribbon.">
|
||||
<div class="wvc-toggle" role="group" aria-label="Waveform ribbon on or off">
|
||||
<MudIconButton Icon="@(ControlState.WaveformEnabled ? DDIcons.LavaLampFilled : DDIcons.LavaLamp)"
|
||||
Color="Color.Primary"
|
||||
OnClick="@ToggleWaveform"
|
||||
aria-label="Waveform ribbon on or off"
|
||||
aria-pressed="@ControlState.WaveformEnabled" />
|
||||
</div>
|
||||
</MudTooltip>
|
||||
|
||||
@* Collisions are the interaction BETWEEN the two subsystems — meaningless with only one
|
||||
present, so visible only when BOTH are on (§3 truth table). *@
|
||||
@if (ControlState.LavaEnabled && ControlState.WaveformEnabled)
|
||||
{
|
||||
<MudTooltip Text="How hard the blobs body-check the beat.">
|
||||
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Collision strength">
|
||||
<RadialKnob Value="@ControlState.CollisionStrength"
|
||||
ValueChanged="@OnCollisionStrengthChanged"
|
||||
Min="0" Max="1" Step="0.001"
|
||||
Size="64"
|
||||
Color="Color.Primary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.Compress" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
||||
</div>
|
||||
</MudTooltip>
|
||||
}
|
||||
</div>
|
||||
|
||||
@* Color applies to the whole field regardless of which subsystems are on, so it is pinned
|
||||
far-right of row 1 and never reflows when collisions hides (§3). *@
|
||||
<MudTooltip Text="How fast the lamp drifts through its colors.">
|
||||
<div class="waveform-visualizer-control mix-visualizer-control wvc-row-right" role="group" aria-label="Color gradient rotation speed">
|
||||
<RadialKnob Value="@ControlState.GradientRotationSpeed"
|
||||
ValueChanged="@OnGradientRotationSpeedChanged"
|
||||
Min="0" Max="1" Step="0.001"
|
||||
Size="64"
|
||||
Color="Color.Primary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.Palette" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
||||
</div>
|
||||
</MudTooltip>
|
||||
</div>
|
||||
|
||||
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Color gradient rotation speed">
|
||||
<RadialKnob Value="@ControlState.GradientRotationSpeed"
|
||||
ValueChanged="@OnGradientRotationSpeedChanged"
|
||||
Min="0" Max="1" Step="0.001"
|
||||
Size="64"
|
||||
Color="Color.Primary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.Palette" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
||||
</div>
|
||||
@* ── Row 2 — LAVA section (only when lava on). ── *@
|
||||
@if (ControlState.LavaEnabled)
|
||||
{
|
||||
<div class="wvc-row wvc-row-section">
|
||||
<span class="wvc-section-label">LAVA:</span>
|
||||
|
||||
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Lava gravity">
|
||||
<RadialKnob Value="@ControlState.LavaGravity"
|
||||
ValueChanged="@OnLavaGravityChanged"
|
||||
Min="0" Max="1" Step="0.001"
|
||||
Size="64"
|
||||
Color="Color.Primary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.ArrowDownward" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
||||
</div>
|
||||
<MudTooltip Text="How heavy the wax feels — float, or sink.">
|
||||
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Lava gravity">
|
||||
<RadialKnob Value="@ControlState.LavaGravity"
|
||||
ValueChanged="@OnLavaGravityChanged"
|
||||
Min="0" Max="1" Step="0.001"
|
||||
Size="64"
|
||||
Color="Color.Primary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.ArrowDownward" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
||||
</div>
|
||||
</MudTooltip>
|
||||
|
||||
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Lava heat">
|
||||
<RadialKnob Value="@ControlState.LavaHeat"
|
||||
ValueChanged="@OnLavaHeatChanged"
|
||||
Min="0" Max="1" Step="0.001"
|
||||
Size="64"
|
||||
Color="Color.Primary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.LocalFireDepartment" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
||||
</div>
|
||||
<MudTooltip Text="Crank the burner. More heat, more rolling boil.">
|
||||
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Lava heat">
|
||||
<RadialKnob Value="@ControlState.LavaHeat"
|
||||
ValueChanged="@OnLavaHeatChanged"
|
||||
Min="0" Max="1" Step="0.001"
|
||||
Size="64"
|
||||
Color="Color.Primary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.LocalFireDepartment" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
||||
</div>
|
||||
</MudTooltip>
|
||||
|
||||
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Fluid amount">
|
||||
<RadialKnob Value="@ControlState.FluidAmount"
|
||||
ValueChanged="@OnFluidAmountChanged"
|
||||
Min="0" Max="1" Step="0.001"
|
||||
Size="64"
|
||||
Color="Color.Primary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.BubbleChart" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
||||
</div>
|
||||
<MudTooltip Text="How much goo is in the lamp.">
|
||||
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Fluid amount">
|
||||
<RadialKnob Value="@ControlState.FluidAmount"
|
||||
ValueChanged="@OnFluidAmountChanged"
|
||||
Min="0" Max="1" Step="0.001"
|
||||
Size="64"
|
||||
Color="Color.Primary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.BubbleChart" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
||||
</div>
|
||||
</MudTooltip>
|
||||
|
||||
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Fluid viscosity">
|
||||
<RadialKnob Value="@ControlState.FluidViscosity"
|
||||
ValueChanged="@OnFluidViscosityChanged"
|
||||
Min="0" Max="1" Step="0.001"
|
||||
Size="64"
|
||||
Color="Color.Primary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.Opacity" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
||||
</div>
|
||||
<MudTooltip Text="Runny and gooey, or tight little globes.">
|
||||
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Fluid viscosity">
|
||||
<RadialKnob Value="@ControlState.FluidViscosity"
|
||||
ValueChanged="@OnFluidViscosityChanged"
|
||||
Min="0" Max="1" Step="0.001"
|
||||
Size="64"
|
||||
Color="Color.Primary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.Opacity" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
||||
</div>
|
||||
</MudTooltip>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Collision strength">
|
||||
<RadialKnob Value="@ControlState.CollisionStrength"
|
||||
ValueChanged="@OnCollisionStrengthChanged"
|
||||
Min="0" Max="1" Step="0.001"
|
||||
Size="64"
|
||||
Color="Color.Primary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.Compress" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
||||
</div>
|
||||
@* ── Row 3 — WAVE section (only when waveform on). Scroll is a SLIDER (§8); width pinned right. ── *@
|
||||
@if (ControlState.WaveformEnabled)
|
||||
{
|
||||
<div class="wvc-row wvc-row-section">
|
||||
<span class="wvc-section-label">WAVE:</span>
|
||||
|
||||
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Waveform width">
|
||||
<RadialKnob Value="@ControlState.WaveformWidth"
|
||||
ValueChanged="@OnWaveformWidthChanged"
|
||||
Min="0" Max="1" Step="0.001"
|
||||
Size="64"
|
||||
Color="Color.Primary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.SettingsEthernet" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
||||
</div>
|
||||
<MudTooltip Text="How fast the sound rolls by.">
|
||||
<div class="wvc-slider" role="group" aria-label="Waveform scroll speed">
|
||||
<MudSlider T="double"
|
||||
Value="@ControlState.ScrollSpeed"
|
||||
ValueChanged="@OnScrollSpeedChanged"
|
||||
Min="0" Max="1" Step="0.001"
|
||||
Color="Color.Primary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.Speed" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
||||
</div>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="How wide the ribbon spreads across the lamp.">
|
||||
<div class="waveform-visualizer-control mix-visualizer-control wvc-row-right" role="group" aria-label="Waveform width">
|
||||
<RadialKnob Value="@ControlState.WaveformWidth"
|
||||
ValueChanged="@OnWaveformWidthChanged"
|
||||
Min="0" Max="1" Step="0.001"
|
||||
Size="64"
|
||||
Color="Color.Primary" />
|
||||
<MudIcon Icon="@Icons.Material.Filled.SettingsEthernet" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
||||
</div>
|
||||
</MudTooltip>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// Whether the knob band is shown. The popover host shows the panel whenever it is open, so the
|
||||
/// default is <c>true</c>. Mix's legacy inline mount still feeds its lava-lamp toggle into this — that
|
||||
/// mount always renders the component, and THIS component decides knob visibility (Phase 10 §4): when
|
||||
/// false the knobs are @if-gated out but the container holds its reserved height (CSS min-height), so
|
||||
/// content below the inline bar never pops as the lamp toggles. Inside the popover the host owns
|
||||
/// Whether the control deck is shown. The overlay host shows the panel whenever it is open, so the
|
||||
/// default is <c>true</c>. Mix's legacy inline mount (if it survives) still feeds its lava-lamp toggle
|
||||
/// into this — that mount always renders the component, and THIS component decides deck visibility
|
||||
/// (Phase 10 §4): when false the rows are @if-gated out but the container holds its reserved height
|
||||
/// (CSS min-height) so content below the inline bar never pops. Inside the overlay the host owns
|
||||
/// open/closed, so the default keeps the panel populated.
|
||||
/// </summary>
|
||||
[Parameter] public bool Visible { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// When <c>true</c>, applies the <c>waveform-visualizer-control-panel</c> class to the root element,
|
||||
/// enabling the global panel-chrome rules (dark-navy ground, border, max-width cap, pinned palette
|
||||
/// tokens). Set by <see cref="WaveformVisualizerControlPopover"/>; Mix's inline mount leaves this
|
||||
/// <c>false</c> so the chrome never leaks onto the inline bar.
|
||||
/// enabling the global panel-chrome rules (NowPlayingCard chrome — square corners, lighter-navy
|
||||
/// ground, thin light border — plus the row/section layout and pinned palette tokens). Set by
|
||||
/// <see cref="WaveformVisualizerControlPopover"/>; Mix's inline mount leaves this <c>false</c> so the
|
||||
/// chrome never leaks onto the inline bar.
|
||||
/// </summary>
|
||||
[Parameter] public bool PanelChrome { get; set; } = false;
|
||||
|
||||
private string _panelChromeClass => PanelChrome ? "waveform-visualizer-control-panel" : string.Empty;
|
||||
|
||||
// Each handler mutates its own dedicated property then raises Changed — the bridge re-reads and
|
||||
// pushes the affected dial. All values are already normalized [0,1]; the bridge maps scroll speed
|
||||
// to a visible time-span and routes the rest straight to the lava/colour dials.
|
||||
// Each handler mutates its own dedicated property then raises Changed — the bridge re-reads and pushes
|
||||
// the affected dial / subsystem-enable. All dial values are already normalized [0,1]; the bridge maps
|
||||
// scroll speed to a visible time-span and routes the rest straight to the lava/colour dials. The two
|
||||
// toggles flip a boolean (no value), driving the genuine per-subsystem draw-skip in the module (§6).
|
||||
|
||||
private void ToggleLava()
|
||||
{
|
||||
ControlState.LavaEnabled = !ControlState.LavaEnabled;
|
||||
ControlState.NotifyChanged();
|
||||
}
|
||||
|
||||
private void ToggleWaveform()
|
||||
{
|
||||
ControlState.WaveformEnabled = !ControlState.WaveformEnabled;
|
||||
ControlState.NotifyChanged();
|
||||
}
|
||||
|
||||
private void OnScrollSpeedChanged(double value)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user