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:
@@ -15,13 +15,12 @@
|
|||||||
TrackEntryKey="@Player?.CurrentTrack?.EntryKey" />
|
TrackEntryKey="@Player?.CurrentTrack?.EntryKey" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@* The lava-lamp popover trigger lands in the panel's top-right corner (full parity, §8e). Above the
|
@* The lava-lamp trigger lands in the panel's top-right corner (full parity, §8e). Above the canvas
|
||||||
canvas and pointer-enabled so the icon is clickable even though the visualizer layer is
|
and pointer-enabled so the icon is clickable even though the visualizer layer is
|
||||||
pointer-events:none. *@
|
pointer-events:none. The panel itself opens screen-centered (Phase 15 §4 — no per-host anchor),
|
||||||
|
so only the icon size is host-specific now. *@
|
||||||
<div class="np-visualizer-controls">
|
<div class="np-visualizer-controls">
|
||||||
<WaveformVisualizerControlPopover IconSize="Size.Small"
|
<WaveformVisualizerControlPopover IconSize="Size.Small" />
|
||||||
AnchorOrigin="Origin.BottomRight"
|
|
||||||
TransformOrigin="Origin.TopLeft" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@* Pulsing rings *@
|
@* Pulsing rings *@
|
||||||
|
|||||||
@@ -265,9 +265,10 @@ public partial class WaveformVisualizer : ComponentBase, IAsyncDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seed the module with the current state now that it exists. All seven control values
|
// Seed the module with the current state now that it exists. All control values (the eight
|
||||||
// come from the shared (session-persisted) state, so a mix opened mid-session seeds the
|
// dials + the two Phase 15 subsystem enables) come from the shared (session-persisted) state,
|
||||||
// module with the knob positions the listener left them at.
|
// so a mix opened mid-session seeds the module with the knob/toggle positions the listener
|
||||||
|
// left them at.
|
||||||
await PushControlsAsync();
|
await PushControlsAsync();
|
||||||
await PushDatumAsync();
|
await PushDatumAsync();
|
||||||
await PushPlaybackAsync();
|
await PushPlaybackAsync();
|
||||||
@@ -291,8 +292,9 @@ public partial class WaveformVisualizer : ComponentBase, IAsyncDisposable
|
|||||||
// ── Bridge pushes. Each is a no-op until the module handle exists. ───────────────────────────
|
// ── Bridge pushes. Each is a no-op until the module handle exists. ───────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Push the eight control values to the module from the shared state. Used to seed on first render
|
/// Push the control values to the module from the shared state — the eight continuous dials plus the
|
||||||
/// and to re-push when the controls bar signals a change. Each value is its own dedicated dial:
|
/// two Phase 15 subsystem enables. Used to seed on first render and to re-push when the controls
|
||||||
|
/// panel signals a change. Each value is its own dedicated dial / enable:
|
||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
/// <item>scroll speed [0,1] is mapped onto the useful zoom band via
|
/// <item>scroll speed [0,1] is mapped onto the useful zoom band via
|
||||||
/// <see cref="WaveformZoomMapping.ScrollKnobToSeconds"/> and pushed through <c>setScrollSpeed</c>
|
/// <see cref="WaveformZoomMapping.ScrollKnobToSeconds"/> and pushed through <c>setScrollSpeed</c>
|
||||||
@@ -302,7 +304,9 @@ public partial class WaveformVisualizer : ComponentBase, IAsyncDisposable
|
|||||||
/// <item>fluid amount → <c>setFluidAmount</c> (blob count + volume); fluid viscosity →
|
/// <item>fluid amount → <c>setFluidAmount</c> (blob count + volume); fluid viscosity →
|
||||||
/// <c>setFluidViscosity</c> (cohesion / sphere-restoration) — the Phase 10 split of the
|
/// <c>setFluidViscosity</c> (cohesion / sphere-restoration) — the Phase 10 split of the
|
||||||
/// former single density knob;</item>
|
/// former single density knob;</item>
|
||||||
/// <item>waveform width → the ribbon-extent uniform.</item>
|
/// <item>waveform width → the ribbon-extent uniform;</item>
|
||||||
|
/// <item>lava / waveform enabled → <c>setLavaEnabled</c> / <c>setWaveformEnabled</c>, the genuine
|
||||||
|
/// per-subsystem draw-skip (no physics / no blob upload, ribbon SDF skipped — §10.1).</item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task PushControlsAsync()
|
private async Task PushControlsAsync()
|
||||||
@@ -319,6 +323,11 @@ public partial class WaveformVisualizer : ComponentBase, IAsyncDisposable
|
|||||||
await _handle.InvokeVoidAsync("setFluidViscosity", ControlState.FluidViscosity);
|
await _handle.InvokeVoidAsync("setFluidViscosity", ControlState.FluidViscosity);
|
||||||
await _handle.InvokeVoidAsync("setCollisionStrength", ControlState.CollisionStrength);
|
await _handle.InvokeVoidAsync("setCollisionStrength", ControlState.CollisionStrength);
|
||||||
await _handle.InvokeVoidAsync("setWaveformWidth", ControlState.WaveformWidth);
|
await _handle.InvokeVoidAsync("setWaveformWidth", ControlState.WaveformWidth);
|
||||||
|
// Phase 15 — the two subsystem enables. "Off" is a genuine draw-skip in the module (no physics,
|
||||||
|
// no blob upload / ribbon SDF skipped), not a dim. Pushed through the same Changed seam as the
|
||||||
|
// dials, so a toggle re-reads here exactly as a knob does.
|
||||||
|
await _handle.InvokeVoidAsync("setLavaEnabled", ControlState.LavaEnabled);
|
||||||
|
await _handle.InvokeVoidAsync("setWaveformEnabled", ControlState.WaveformEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,56 +1,56 @@
|
|||||||
@namespace DeepDrftPublic.Client.Controls
|
@namespace DeepDrftPublic.Client.Controls
|
||||||
@using DeepDrftShared.Client.Common
|
@using DeepDrftShared.Client.Common
|
||||||
|
|
||||||
@* The single controls affordance, placed by an icon anywhere (Phase 12 §3d-revised). Closed state is
|
@* The single controls affordance, placed by an icon anywhere (Phase 12 §3d-revised, re-primitived
|
||||||
just the lava-lamp icon; clicking it floats the eight-knob WaveformVisualizerControls panel over the
|
Phase 15 §4). Closed state is just the lava-lamp icon; clicking it floats the control panel as a
|
||||||
surface. One panel, one popover host, reused on every host (Mix, Cut, Session, NowPlaying card) — the
|
SCREEN-CENTERED, tinted MODAL over the whole surface. One panel, one host, reused on every host (Mix,
|
||||||
SOLID seam: variance is the per-host anchor (§8e), never a forked popover.
|
Cut, Session, NowPlaying) — the SOLID seam.
|
||||||
|
|
||||||
Anchoring follows the SharePopover precedent: Fixed so the panel reads the trigger's bounding rect
|
PRIMITIVE (Phase 15 §4): a centered MudOverlay, NOT an anchored MudPopover. The panel must read as
|
||||||
rather than fighting CSS container tricks. AnchorOrigin/TransformOrigin are per-host
|
screen-centered regardless of where the lava-lamp icon sits (Mix corner, Cut/Session ambient,
|
||||||
parameters (§8e) defaulted to bottom-right open-down — the cleanest case (Mix's TopRightAction corner);
|
NowPlaying corner). An anchored popover positions off the trigger's bounding rect — the wrong model
|
||||||
tight hosts (the NowPlaying card) override to open away from the card body.
|
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
|
KNOB-DRAG SAFETY (Phase 15 §4, highest-risk detail): RadialKnob mounts its own full-viewport
|
||||||
the shared WaveformVisualizerControlState and raises Changed; the visualizer bridge subscribes. This host
|
position:fixed; z-index:9999 mouse-capture div WHILE dragging (RadialKnob.razor lines 5–9). That capture
|
||||||
only toggles open/closed and places the panel — it stays purely presentational. *@
|
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
|
The host owns NO control state and NO JS interop. The hosted WaveformVisualizerControls panel mutates
|
||||||
off so a knob drag (which can land pointer-up outside the panel's DOM subtree) does not dismiss. *@
|
the shared WaveformVisualizerControlState and raises Changed; the visualizer bridge subscribes. This
|
||||||
<MudOverlay Visible="@_open" OnClick="@Close" />
|
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. *@
|
<MudTooltip Text="Visualizer settings">
|
||||||
<div>
|
<MudIconButton Icon="@(_open ? DDIcons.LavaLampFilled : DDIcons.LavaLamp)"
|
||||||
<MudTooltip Text="Visualizer settings">
|
Size="@IconSize"
|
||||||
<MudIconButton Icon="@(_open ? DDIcons.LavaLampFilled : DDIcons.LavaLamp)"
|
Color="Color.Secondary"
|
||||||
Size="@IconSize"
|
Disabled="@(!RendererInfo.IsInteractive)"
|
||||||
Color="Color.Secondary"
|
OnClick="@Toggle"
|
||||||
Disabled="@(!RendererInfo.IsInteractive)"
|
aria-label="Visualizer settings"
|
||||||
OnClick="@Toggle"
|
aria-expanded="@_open" />
|
||||||
aria-label="Visualizer settings"
|
</MudTooltip>
|
||||||
aria-expanded="@_open" />
|
|
||||||
</MudTooltip>
|
|
||||||
|
|
||||||
<MudPopover Open="@_open"
|
@* The tinted modal scrim that also HOLDS the panel. DarkBackground = the mild tint; OnClick on the scrim
|
||||||
Fixed="true"
|
dismisses (knob-drag-safe, see header). The panel is the overlay's centered child; it stops click
|
||||||
AnchorOrigin="@AnchorOrigin"
|
propagation so an inside click is not a dismissal. Modal so focus/scroll stay on the panel. *@
|
||||||
TransformOrigin="@TransformOrigin"
|
<MudOverlay Visible="@_open"
|
||||||
Class="waveform-visualizer-control-popover">
|
DarkBackground="true"
|
||||||
|
Modal="true"
|
||||||
|
OnClick="@Close"
|
||||||
|
Class="waveform-visualizer-control-overlay">
|
||||||
|
<div class="waveform-visualizer-control-modal" @onclick:stopPropagation="true">
|
||||||
<WaveformVisualizerControls PanelChrome="true" />
|
<WaveformVisualizerControls PanelChrome="true" />
|
||||||
</MudPopover>
|
</div>
|
||||||
</div>
|
</MudOverlay>
|
||||||
|
|
||||||
@code {
|
@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>
|
/// <summary>Trigger-icon size. Defaults Large to match the Phase 10 Mix lava-lamp button.</summary>
|
||||||
[Parameter] public Size IconSize { get; set; } = Size.Large;
|
[Parameter] public Size IconSize { get; set; } = Size.Large;
|
||||||
|
|
||||||
|
|||||||
@@ -1,139 +1,221 @@
|
|||||||
@namespace DeepDrftPublic.Client.Controls
|
@namespace DeepDrftPublic.Client.Controls
|
||||||
|
@using DeepDrftShared.Client.Common
|
||||||
@using DeepDrftPublic.Client.Services
|
@using DeepDrftPublic.Client.Services
|
||||||
@inject WaveformVisualizerControlState ControlState
|
@inject WaveformVisualizerControlState ControlState
|
||||||
|
|
||||||
@* The waveform visualizer control PANEL (Phase 12 §3d-revised). EIGHT continuous RadialKnobs — scroll
|
@* The waveform visualizer control PANEL (Phase 12 §3d-revised → Phase 15 re-layout). The control deck for
|
||||||
speed, gradient rotation speed, lava gravity, lava heat, fluid amount, fluid viscosity, collision
|
the lava-lamp visualizer: a deterministic THREE-ROW, sectioned layout that encodes what the visualizer
|
||||||
strength, waveform width — each its own dedicated control with a Material-icon caption. The single
|
composes — a LAVA field and a WAVEFORM ribbon, each independently toggleable (Phase 15 §3):
|
||||||
"bubbles" knob is split into fluid-amount + fluid-viscosity (Phase 10 §5).
|
|
||||||
|
|
||||||
This component is the PANEL CONTENT hosted inside WaveformVisualizerControlPopover. It no longer rides
|
Row 1 (MODE, always): lava lamp-toggle, waveform lamp-toggle, [collisions knob — only when BOTH on],
|
||||||
an inline TopRowCenter bar; it lays out as a wrapped grid sized for a popover, styled to the NowPlaying
|
then the color knob pinned far-right (applies to the whole field, so always visible).
|
||||||
Hero look (§3g — dark-navy ground, green-accent knobs, light icons) from the deepdrft-* tokens.
|
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
|
The two lamp toggles are iconographic (lit LavaLampFilled / unlit LavaLamp glyph), green because they
|
||||||
not reach the rendered panel — so panel chrome lives in the GLOBAL deepdrft-styles.css
|
are INTERACTIVE (the §5 colour principle: green = interactive, light = non-interactive). The eight
|
||||||
(.waveform-visualizer-control-panel*), not in the scoped .razor.css. The scoped .razor.css carries only
|
continuous dials are unchanged in tuning; the one widget-type change is scroll/zoom → a MudSlider (§8,
|
||||||
the inline-bar fallback (the .mix-visualizer-controls-bar reserved-height row) Mix's existing
|
bound to ScrollSpeed alone). None of these is a seek surface (read-only contract §D).
|
||||||
TopRowCenter mount still uses, which is NOT portaled and so resolves under isolation.
|
|
||||||
|
|
||||||
Visibility: the popover host always shows the panel when open (Visible defaults true). Mix's legacy
|
This is the PANEL CONTENT hosted inside WaveformVisualizerControlPopover, now a screen-centered tinted
|
||||||
inline mount still feeds its lava-lamp toggle into Visible (Phase 10 §4): the knobs @if-gate while the
|
MudOverlay (Phase 15 §4). Because the overlay PORTALS its content out of this component's DOM subtree,
|
||||||
container holds a reserved min-height so content below never pops on toggle.
|
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
|
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
|
its Changed event. The visualizer bridge (WaveformVisualizer) subscribes and pushes the affected dial /
|
||||||
affected dial to the WebGL module. That keeps the JS module handle single-owned by the bridge and this
|
subsystem-enable to the WebGL module. RadialKnob has no icon slot (its Label renders as SVG text) and no
|
||||||
component purely presentational. None of these is a seek surface (read-only contract §D).
|
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. *@
|
||||||
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. *@
|
|
||||||
|
|
||||||
<div class="@($"{_panelChromeClass} mix-visualizer-controls-bar".TrimStart())">
|
<div class="@($"{_panelChromeClass} mix-visualizer-controls-bar".TrimStart())">
|
||||||
|
|
||||||
@if (Visible)
|
@if (Visible)
|
||||||
{
|
{
|
||||||
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Waveform scroll speed">
|
@* ── Row 1 — MODE (always visible). Toggles + collisions group left; color pinned right. ── *@
|
||||||
<RadialKnob Value="@ControlState.ScrollSpeed"
|
<div class="wvc-row wvc-row-mode">
|
||||||
ValueChanged="@OnScrollSpeedChanged"
|
<div class="wvc-row-left">
|
||||||
Min="0" Max="1" Step="0.001"
|
|
||||||
Size="64"
|
<MudTooltip Text="Light the lamp — or let it go cold.">
|
||||||
Color="Color.Primary" />
|
<div class="wvc-toggle" role="group" aria-label="Lava field on or off">
|
||||||
<MudIcon Icon="@Icons.Material.Filled.Speed" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
<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>
|
||||||
|
|
||||||
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Color gradient rotation speed">
|
@* ── Row 2 — LAVA section (only when lava on). ── *@
|
||||||
<RadialKnob Value="@ControlState.GradientRotationSpeed"
|
@if (ControlState.LavaEnabled)
|
||||||
ValueChanged="@OnGradientRotationSpeedChanged"
|
{
|
||||||
Min="0" Max="1" Step="0.001"
|
<div class="wvc-row wvc-row-section">
|
||||||
Size="64"
|
<span class="wvc-section-label">LAVA:</span>
|
||||||
Color="Color.Primary" />
|
|
||||||
<MudIcon Icon="@Icons.Material.Filled.Palette" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Lava gravity">
|
<MudTooltip Text="How heavy the wax feels — float, or sink.">
|
||||||
<RadialKnob Value="@ControlState.LavaGravity"
|
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Lava gravity">
|
||||||
ValueChanged="@OnLavaGravityChanged"
|
<RadialKnob Value="@ControlState.LavaGravity"
|
||||||
Min="0" Max="1" Step="0.001"
|
ValueChanged="@OnLavaGravityChanged"
|
||||||
Size="64"
|
Min="0" Max="1" Step="0.001"
|
||||||
Color="Color.Primary" />
|
Size="64"
|
||||||
<MudIcon Icon="@Icons.Material.Filled.ArrowDownward" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
Color="Color.Primary" />
|
||||||
</div>
|
<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">
|
<MudTooltip Text="Crank the burner. More heat, more rolling boil.">
|
||||||
<RadialKnob Value="@ControlState.LavaHeat"
|
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Lava heat">
|
||||||
ValueChanged="@OnLavaHeatChanged"
|
<RadialKnob Value="@ControlState.LavaHeat"
|
||||||
Min="0" Max="1" Step="0.001"
|
ValueChanged="@OnLavaHeatChanged"
|
||||||
Size="64"
|
Min="0" Max="1" Step="0.001"
|
||||||
Color="Color.Primary" />
|
Size="64"
|
||||||
<MudIcon Icon="@Icons.Material.Filled.LocalFireDepartment" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
Color="Color.Primary" />
|
||||||
</div>
|
<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">
|
<MudTooltip Text="How much goo is in the lamp.">
|
||||||
<RadialKnob Value="@ControlState.FluidAmount"
|
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Fluid amount">
|
||||||
ValueChanged="@OnFluidAmountChanged"
|
<RadialKnob Value="@ControlState.FluidAmount"
|
||||||
Min="0" Max="1" Step="0.001"
|
ValueChanged="@OnFluidAmountChanged"
|
||||||
Size="64"
|
Min="0" Max="1" Step="0.001"
|
||||||
Color="Color.Primary" />
|
Size="64"
|
||||||
<MudIcon Icon="@Icons.Material.Filled.BubbleChart" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
Color="Color.Primary" />
|
||||||
</div>
|
<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">
|
<MudTooltip Text="Runny and gooey, or tight little globes.">
|
||||||
<RadialKnob Value="@ControlState.FluidViscosity"
|
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Fluid viscosity">
|
||||||
ValueChanged="@OnFluidViscosityChanged"
|
<RadialKnob Value="@ControlState.FluidViscosity"
|
||||||
Min="0" Max="1" Step="0.001"
|
ValueChanged="@OnFluidViscosityChanged"
|
||||||
Size="64"
|
Min="0" Max="1" Step="0.001"
|
||||||
Color="Color.Primary" />
|
Size="64"
|
||||||
<MudIcon Icon="@Icons.Material.Filled.Opacity" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
Color="Color.Primary" />
|
||||||
</div>
|
<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">
|
@* ── Row 3 — WAVE section (only when waveform on). Scroll is a SLIDER (§8); width pinned right. ── *@
|
||||||
<RadialKnob Value="@ControlState.CollisionStrength"
|
@if (ControlState.WaveformEnabled)
|
||||||
ValueChanged="@OnCollisionStrengthChanged"
|
{
|
||||||
Min="0" Max="1" Step="0.001"
|
<div class="wvc-row wvc-row-section">
|
||||||
Size="64"
|
<span class="wvc-section-label">WAVE:</span>
|
||||||
Color="Color.Primary" />
|
|
||||||
<MudIcon Icon="@Icons.Material.Filled.Compress" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="waveform-visualizer-control mix-visualizer-control" role="group" aria-label="Waveform width">
|
<MudTooltip Text="How fast the sound rolls by.">
|
||||||
<RadialKnob Value="@ControlState.WaveformWidth"
|
<div class="wvc-slider" role="group" aria-label="Waveform scroll speed">
|
||||||
ValueChanged="@OnWaveformWidthChanged"
|
<MudSlider T="double"
|
||||||
Min="0" Max="1" Step="0.001"
|
Value="@ControlState.ScrollSpeed"
|
||||||
Size="64"
|
ValueChanged="@OnScrollSpeedChanged"
|
||||||
Color="Color.Primary" />
|
Min="0" Max="1" Step="0.001"
|
||||||
<MudIcon Icon="@Icons.Material.Filled.SettingsEthernet" Size="Size.Small" Class="waveform-visualizer-control-icon mix-visualizer-control-icon" />
|
Color="Color.Primary" />
|
||||||
</div>
|
<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>
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the knob band is shown. The popover host shows the panel whenever it is open, so the
|
/// 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 still feeds its lava-lamp toggle into this — that
|
/// default is <c>true</c>. Mix's legacy inline mount (if it survives) still feeds its lava-lamp toggle
|
||||||
/// mount always renders the component, and THIS component decides knob visibility (Phase 10 §4): when
|
/// into this — that mount always renders the component, and THIS component decides deck visibility
|
||||||
/// false the knobs are @if-gated out but the container holds its reserved height (CSS min-height), so
|
/// (Phase 10 §4): when false the rows are @if-gated out but the container holds its reserved height
|
||||||
/// content below the inline bar never pops as the lamp toggles. Inside the popover the host owns
|
/// (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.
|
/// open/closed, so the default keeps the panel populated.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Parameter] public bool Visible { get; set; } = true;
|
[Parameter] public bool Visible { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// When <c>true</c>, applies the <c>waveform-visualizer-control-panel</c> class to the root element,
|
/// 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
|
/// enabling the global panel-chrome rules (NowPlayingCard chrome — square corners, lighter-navy
|
||||||
/// tokens). Set by <see cref="WaveformVisualizerControlPopover"/>; Mix's inline mount leaves this
|
/// ground, thin light border — plus the row/section layout and pinned palette tokens). Set by
|
||||||
/// <c>false</c> so the chrome never leaks onto the inline bar.
|
/// <see cref="WaveformVisualizerControlPopover"/>; Mix's inline mount leaves this <c>false</c> so the
|
||||||
|
/// chrome never leaks onto the inline bar.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Parameter] public bool PanelChrome { get; set; } = false;
|
[Parameter] public bool PanelChrome { get; set; } = false;
|
||||||
|
|
||||||
private string _panelChromeClass => PanelChrome ? "waveform-visualizer-control-panel" : string.Empty;
|
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
|
// Each handler mutates its own dedicated property then raises Changed — the bridge re-reads and pushes
|
||||||
// pushes the affected dial. All values are already normalized [0,1]; the bridge maps scroll speed
|
// the affected dial / subsystem-enable. All dial values are already normalized [0,1]; the bridge maps
|
||||||
// to a visible time-span and routes the rest straight to the lava/colour dials.
|
// 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)
|
private void OnScrollSpeedChanged(double value)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
namespace DeepDrftPublic.Client.Services;
|
namespace DeepDrftPublic.Client.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Holds the waveform visualizer's eight continuous-control positions for the lifetime of the WASM app
|
/// Holds the waveform visualizer's eight continuous-control positions plus two subsystem on/off
|
||||||
/// instance. Scoped in DI, so it lives across SPA navigations within one listening session — open a
|
/// toggles for the lifetime of the WASM app instance. Scoped in DI, so it lives across SPA navigations within one listening session — open a
|
||||||
/// second mix and the knobs keep where you left them — but a fresh page load (F5) constructs a new
|
/// second mix and the knobs keep where you left them — but a fresh page load (F5) constructs a new
|
||||||
/// instance, resetting to defaults. That matches the spec's "persist within session, reset on fresh
|
/// instance, resetting to defaults. That matches the spec's "persist within session, reset on fresh
|
||||||
/// load" without any cookie/localStorage round-trip (lava reframe §7c).
|
/// load" without any cookie/localStorage round-trip (lava reframe §7c).
|
||||||
@@ -84,6 +84,19 @@ public sealed class WaveformVisualizerControlState
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const double DefaultWaveformWidth = 0.5;
|
public const double DefaultWaveformWidth = 0.5;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default lava-subsystem on-state. <c>true</c> so the lava field is on out of the box — the
|
||||||
|
/// current behavior. Backs the row-1 lava lamp toggle (Phase 15 §6). Has no TS-side anchor: the
|
||||||
|
/// bridge pushes it as an enable/disable, not a tuning dial.
|
||||||
|
/// </summary>
|
||||||
|
public const bool DefaultLavaEnabled = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default waveform-subsystem on-state. <c>true</c> so the waveform ribbon is on out of the box.
|
||||||
|
/// Backs the row-1 waveform lamp toggle (Phase 15 §6).
|
||||||
|
/// </summary>
|
||||||
|
public const bool DefaultWaveformEnabled = true;
|
||||||
|
|
||||||
/// <summary>Apparent bottom-to-top scroll rate, normalized [0,1]. Bridge maps it to a visible
|
/// <summary>Apparent bottom-to-top scroll rate, normalized [0,1]. Bridge maps it to a visible
|
||||||
/// time-span via <see cref="WaveformZoomMapping"/>; the standalone resolution/zoom control is gone.</summary>
|
/// time-span via <see cref="WaveformZoomMapping"/>; the standalone resolution/zoom control is gone.</summary>
|
||||||
public double ScrollSpeed { get; set; } = DefaultScrollSpeed;
|
public double ScrollSpeed { get; set; } = DefaultScrollSpeed;
|
||||||
@@ -110,6 +123,20 @@ public sealed class WaveformVisualizerControlState
|
|||||||
/// <summary>Waveform-band horizontal extent, normalized [0,1]. Narrowing clears room for the lava.</summary>
|
/// <summary>Waveform-band horizontal extent, normalized [0,1]. Narrowing clears room for the lava.</summary>
|
||||||
public double WaveformWidth { get; set; } = DefaultWaveformWidth;
|
public double WaveformWidth { get; set; } = DefaultWaveformWidth;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the lava field is drawn. When <c>false</c> the lava subsystem is genuinely not rendered
|
||||||
|
/// (the bridge skips its physics + uploads no blobs — no render cost, Phase 15 §6/§10.1), not dimmed.
|
||||||
|
/// Also gates the row-1/row-2 control visibility (§3).
|
||||||
|
/// </summary>
|
||||||
|
public bool LavaEnabled { get; set; } = DefaultLavaEnabled;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the waveform ribbon is drawn. When <c>false</c> the ribbon subsystem is genuinely not
|
||||||
|
/// rendered (the bridge disables the ribbon SDF + drops its collision boundary — no render cost,
|
||||||
|
/// Phase 15 §6/§10.1), not dimmed. Also gates the row-1/row-3 control visibility (§3).
|
||||||
|
/// </summary>
|
||||||
|
public bool WaveformEnabled { get; set; } = DefaultWaveformEnabled;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Raised whenever any control value changes. The visualizer bridge subscribes to push the
|
/// Raised whenever any control value changes. The visualizer bridge subscribes to push the
|
||||||
/// affected dial(s). Mutators set the property then raise this; subscribers re-read the values.
|
/// affected dial(s). Mutators set the property then raise this; subscribers re-read the values.
|
||||||
|
|||||||
@@ -527,6 +527,19 @@ export interface WaveformVisualizerHandle {
|
|||||||
setCollisionStrength(value: number): void;
|
setCollisionStrength(value: number): void;
|
||||||
/** [0,1]. Waveform-band horizontal extent (1 = full ribbon, lower narrows). */
|
/** [0,1]. Waveform-band horizontal extent (1 = full ribbon, lower narrows). */
|
||||||
setWaveformWidth(value: number): void;
|
setWaveformWidth(value: number): void;
|
||||||
|
/**
|
||||||
|
* Enable/disable the LAVA subsystem (Phase 15). When disabled the wax is genuinely NOT rendered:
|
||||||
|
* the per-frame physics step is skipped and zero blobs are uploaded (uBlobCount = 0), so the
|
||||||
|
* shader's blob loop unions nothing — no render cost, not a dimmed/visible=false uniform (§10.1).
|
||||||
|
*/
|
||||||
|
setLavaEnabled(enabled: boolean): void;
|
||||||
|
/**
|
||||||
|
* Enable/disable the WAVEFORM-ribbon subsystem (Phase 15). When disabled the ribbon SDF is skipped
|
||||||
|
* in the shader (uWaveformEnabled = 0 makes waveformSdf return "fully outside") and its CPU
|
||||||
|
* collision boundary is dropped (sampleLoudnessAt reads 0), so the ribbon contributes nothing to
|
||||||
|
* the surface and the wax stops bouncing off an invisible wall — a genuine skip, not a dim (§10.1).
|
||||||
|
*/
|
||||||
|
setWaveformEnabled(enabled: boolean): void;
|
||||||
/** Re-read the palette CSS vars off the canvas (call after a dark-mode toggle). */
|
/** Re-read the palette CSS vars off the canvas (call after a dark-mode toggle). */
|
||||||
refreshTheme(): void;
|
refreshTheme(): void;
|
||||||
dispose(): void;
|
dispose(): void;
|
||||||
@@ -613,6 +626,8 @@ uniform float uPlayheadSeconds; // current playback position (per-frame)
|
|||||||
uniform float uTimeSeconds; // monotonic clock (per-frame) — drives field morph
|
uniform float uTimeSeconds; // monotonic clock (per-frame) — drives field morph
|
||||||
uniform float uVisibleSeconds; // zoom: window time-span (per change)
|
uniform float uVisibleSeconds; // zoom: window time-span (per change)
|
||||||
uniform float uWaveformWidth; // [0,1] R2: scales the ribbon half-width (narrow the band for lava room)
|
uniform float uWaveformWidth; // [0,1] R2: scales the ribbon half-width (narrow the band for lava room)
|
||||||
|
uniform float uWaveformEnabled; // [0,1] Phase 15: 1 = ribbon drawn, 0 = ribbon subsystem skipped (no
|
||||||
|
// contribution to the surface — see waveformSdf's early-out)
|
||||||
uniform float uCohesion; // [0,1] Phase 10: fluid viscosity/cohesion — high = crisp spheres,
|
uniform float uCohesion; // [0,1] Phase 10: fluid viscosity/cohesion — high = crisp spheres,
|
||||||
// low = gooey/deformed (drives the smin blend width + wobble below)
|
// low = gooey/deformed (drives the smin blend width + wobble below)
|
||||||
// NOTE: the lava physics params (gravity/heat/collision/density) are NOT shader uniforms
|
// NOTE: the lava physics params (gravity/heat/collision/density) are NOT shader uniforms
|
||||||
@@ -877,6 +892,10 @@ vec3 anchorAtPhase(float phase) {
|
|||||||
// distance to that vertical ribbon band. Loudness at neighbour rows is NOT re-stacked
|
// distance to that vertical ribbon band. Loudness at neighbour rows is NOT re-stacked
|
||||||
// here (the per-row geometry from Wave 1 is already smooth); the band is the ribbon.
|
// here (the per-row geometry from Wave 1 is already smooth); the band is the ribbon.
|
||||||
float waveformSdf(vec2 p, float aspect, float nowYn, float secondsPerHeight) {
|
float waveformSdf(vec2 p, float aspect, float nowYn, float secondsPerHeight) {
|
||||||
|
// Phase 15: ribbon subsystem off → return "fully outside" so the smin union ignores it entirely
|
||||||
|
// (a far positive distance never pulls the surface toward the centre line). This is the genuine
|
||||||
|
// skip — the ribbon contributes nothing, rather than being drawn-then-hidden.
|
||||||
|
if (uWaveformEnabled < 0.5) return 1e9;
|
||||||
// Mix-time at this row: rows below the now-line are future audio, above are past.
|
// Mix-time at this row: rows below the now-line are future audio, above are past.
|
||||||
float t = uPlayheadSeconds + (p.y - nowYn) * secondsPerHeight;
|
float t = uPlayheadSeconds + (p.y - nowYn) * secondsPerHeight;
|
||||||
float amp = sampleAt(t); // loudness 0..1 at this row
|
float amp = sampleAt(t); // loudness 0..1 at this row
|
||||||
@@ -1072,6 +1091,8 @@ function noopHandle(): WaveformVisualizerHandle {
|
|||||||
setFluidViscosity() {},
|
setFluidViscosity() {},
|
||||||
setCollisionStrength() {},
|
setCollisionStrength() {},
|
||||||
setWaveformWidth() {},
|
setWaveformWidth() {},
|
||||||
|
setLavaEnabled() {},
|
||||||
|
setWaveformEnabled() {},
|
||||||
refreshTheme() {},
|
refreshTheme() {},
|
||||||
dispose() {},
|
dispose() {},
|
||||||
};
|
};
|
||||||
@@ -1129,6 +1150,7 @@ export function create(canvas: HTMLCanvasElement): WaveformVisualizerHandle {
|
|||||||
timeSeconds: gl.getUniformLocation(program, 'uTimeSeconds'),
|
timeSeconds: gl.getUniformLocation(program, 'uTimeSeconds'),
|
||||||
visibleSeconds: gl.getUniformLocation(program, 'uVisibleSeconds'),
|
visibleSeconds: gl.getUniformLocation(program, 'uVisibleSeconds'),
|
||||||
waveformWidth: gl.getUniformLocation(program, 'uWaveformWidth'),
|
waveformWidth: gl.getUniformLocation(program, 'uWaveformWidth'),
|
||||||
|
waveformEnabled: gl.getUniformLocation(program, 'uWaveformEnabled'),
|
||||||
cohesion: gl.getUniformLocation(program, 'uCohesion'),
|
cohesion: gl.getUniformLocation(program, 'uCohesion'),
|
||||||
durationSeconds: gl.getUniformLocation(program, 'uDurationSeconds'),
|
durationSeconds: gl.getUniformLocation(program, 'uDurationSeconds'),
|
||||||
colorNavy: gl.getUniformLocation(program, 'uColorNavy'),
|
colorNavy: gl.getUniformLocation(program, 'uColorNavy'),
|
||||||
@@ -1167,6 +1189,12 @@ export function create(canvas: HTMLCanvasElement): WaveformVisualizerHandle {
|
|||||||
let waveformWidth = DEFAULT_WAVEFORM_WIDTH;
|
let waveformWidth = DEFAULT_WAVEFORM_WIDTH;
|
||||||
// LIVE as of Wave R3 — drives the gradient anchor-rotation rate (Motion 1).
|
// LIVE as of Wave R3 — drives the gradient anchor-rotation rate (Motion 1).
|
||||||
let gradientRotationSpeed = DEFAULT_GRADIENT_ROTATION_SPEED;
|
let gradientRotationSpeed = DEFAULT_GRADIENT_ROTATION_SPEED;
|
||||||
|
// Phase 15 — subsystem on/off. Default ON (mirrors C# DefaultLavaEnabled / DefaultWaveformEnabled),
|
||||||
|
// so out of the box both subsystems run exactly as before. "Off" is a genuine draw-skip: lava off
|
||||||
|
// skips stepPhysics + uploads zero blobs; waveform off skips the ribbon SDF (uWaveformEnabled) and
|
||||||
|
// its CPU collision boundary. With both off, draw() short-circuits to a clear — no SDF eval at all.
|
||||||
|
let lavaEnabled = true;
|
||||||
|
let waveformEnabled = true;
|
||||||
|
|
||||||
/** Effective ribbon-width fraction for the current width knob (Phase 10 §3.7): the knob's [0,1]
|
/** Effective ribbon-width fraction for the current width knob (Phase 10 §3.7): the knob's [0,1]
|
||||||
* travel maps onto the useful 10%–95% band (full-width 100% read too wide; sub-10% vanished).
|
* travel maps onto the useful 10%–95% band (full-width 100% read too wide; sub-10% vanished).
|
||||||
@@ -1365,6 +1393,9 @@ export function create(canvas: HTMLCanvasElement): WaveformVisualizerHandle {
|
|||||||
* boundary matches the rendered waveform exactly. Reads the retained datum.samples.
|
* boundary matches the rendered waveform exactly. Reads the retained datum.samples.
|
||||||
*/
|
*/
|
||||||
function sampleLoudnessAt(timeSeconds: number): number {
|
function sampleLoudnessAt(timeSeconds: number): number {
|
||||||
|
// Phase 15: waveform off → no ribbon boundary. Reporting zero loudness collapses the collision
|
||||||
|
// half-width to 0, so wax never bounces off an invisible wall (matches the skipped ribbon draw).
|
||||||
|
if (!waveformEnabled) return 0;
|
||||||
const d = datum;
|
const d = datum;
|
||||||
if (!d || timeSeconds < 0 || timeSeconds >= d.durationSeconds) return 0;
|
if (!d || timeSeconds < 0 || timeSeconds >= d.durationSeconds) return 0;
|
||||||
const n = d.sampleCount;
|
const n = d.sampleCount;
|
||||||
@@ -1731,6 +1762,14 @@ export function create(canvas: HTMLCanvasElement): WaveformVisualizerHandle {
|
|||||||
gl.clearColor(0, 0, 0, 0);
|
gl.clearColor(0, 0, 0, 0);
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
|
// Phase 15 — both subsystems off: there is nothing to draw. Short-circuit past the physics
|
||||||
|
// step, the blob upload, and the full-screen SDF evaluation entirely — a genuine no-render-cost
|
||||||
|
// empty field (§10.1), not a shader that runs and outputs transparent. The cleared (transparent)
|
||||||
|
// buffer above is the result. The gradient/playhead clocks are not advanced while fully off;
|
||||||
|
// they resume from their held value when a subsystem is turned back on (no visible snap, since
|
||||||
|
// an off field shows nothing to snap).
|
||||||
|
if (!lavaEnabled && !waveformEnabled) return;
|
||||||
|
|
||||||
gl.useProgram(program);
|
gl.useProgram(program);
|
||||||
gl.bindVertexArray(vao);
|
gl.bindVertexArray(vao);
|
||||||
|
|
||||||
@@ -1756,6 +1795,7 @@ export function create(canvas: HTMLCanvasElement): WaveformVisualizerHandle {
|
|||||||
// separate dirty-tracking needed for scalars/vec3s).
|
// separate dirty-tracking needed for scalars/vec3s).
|
||||||
gl.uniform1f(u.visibleSeconds, visibleSeconds);
|
gl.uniform1f(u.visibleSeconds, visibleSeconds);
|
||||||
gl.uniform1f(u.waveformWidth, effectiveWaveformWidth());
|
gl.uniform1f(u.waveformWidth, effectiveWaveformWidth());
|
||||||
|
gl.uniform1f(u.waveformEnabled, waveformEnabled ? 1 : 0);
|
||||||
gl.uniform1f(u.cohesion, fluidViscosity);
|
gl.uniform1f(u.cohesion, fluidViscosity);
|
||||||
gl.uniform1f(u.gradientPhase, gradientPhase);
|
gl.uniform1f(u.gradientPhase, gradientPhase);
|
||||||
gl.uniform3fv(u.colorNavy, theme.navy);
|
gl.uniform3fv(u.colorNavy, theme.navy);
|
||||||
@@ -1769,8 +1809,15 @@ export function create(canvas: HTMLCanvasElement): WaveformVisualizerHandle {
|
|||||||
const nowMs = performance.now();
|
const nowMs = performance.now();
|
||||||
const physicsDt = Math.max(0, (nowMs - lastPhysicsMs) / 1000);
|
const physicsDt = Math.max(0, (nowMs - lastPhysicsMs) / 1000);
|
||||||
lastPhysicsMs = nowMs;
|
lastPhysicsMs = nowMs;
|
||||||
stepPhysics(physicsDt);
|
// Phase 15 — lava off: skip the CPU physics step AND upload zero blobs. The shader's blob loop
|
||||||
const liveCount = packBlobs();
|
// (`for … if (i >= uBlobCount) break;`) then unions nothing, so no wax is drawn and no physics
|
||||||
|
// runs — a genuine subsystem skip (§10.1), not a hidden-but-simulated field. The wax keeps its
|
||||||
|
// last positions for free (we just stop integrating); turning lava back on resumes from there.
|
||||||
|
let liveCount = 0;
|
||||||
|
if (lavaEnabled) {
|
||||||
|
stepPhysics(physicsDt);
|
||||||
|
liveCount = packBlobs();
|
||||||
|
}
|
||||||
gl.uniform4fv(u.blobs, blobUpload);
|
gl.uniform4fv(u.blobs, blobUpload);
|
||||||
gl.uniform1i(u.blobCount, liveCount);
|
gl.uniform1i(u.blobCount, liveCount);
|
||||||
|
|
||||||
@@ -2156,6 +2203,22 @@ export function create(canvas: HTMLCanvasElement): WaveformVisualizerHandle {
|
|||||||
if (rafId === null) redrawOnce();
|
if (rafId === null) redrawOnce();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Phase 15 — subsystem enables. "Off" is a genuine draw-skip (§10.1): lava off stops the physics
|
||||||
|
// step + uploads zero blobs (handled in draw()); waveform off skips the ribbon SDF + collision
|
||||||
|
// boundary. redrawOnce guards the fully-stopped (tab-hidden) case so the toggle lands a still
|
||||||
|
// frame when the loop resumes — including the both-off → cleared empty field.
|
||||||
|
setLavaEnabled(enabled: boolean): void {
|
||||||
|
lavaEnabled = enabled;
|
||||||
|
debugLog(`setLavaEnabled → ${enabled}.`);
|
||||||
|
if (rafId === null) redrawOnce();
|
||||||
|
},
|
||||||
|
|
||||||
|
setWaveformEnabled(enabled: boolean): void {
|
||||||
|
waveformEnabled = enabled;
|
||||||
|
debugLog(`setWaveformEnabled → ${enabled}.`);
|
||||||
|
if (rafId === null) redrawOnce();
|
||||||
|
},
|
||||||
|
|
||||||
refreshTheme(): void {
|
refreshTheme(): void {
|
||||||
theme = readTheme();
|
theme = readTheme();
|
||||||
if (rafId === null) redrawOnce();
|
if (rafId === null) redrawOnce();
|
||||||
|
|||||||
@@ -376,49 +376,144 @@ h2, h3, h4, h5, h6,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* =============================================================================
|
/* =============================================================================
|
||||||
WAVEFORM VISUALIZER CONTROL PANEL (Phase 12 §3d-revised / §3g)
|
WAVEFORM VISUALIZER CONTROL PANEL (Phase 12 §3d-revised / §3g → Phase 15 re-layout)
|
||||||
The eight-knob panel hosted inside WaveformVisualizerControlPopover. MudPopover
|
The control deck hosted inside WaveformVisualizerControlPopover, now a screen-centered
|
||||||
PORTALS its content out of the component's DOM subtree, so Blazor CSS isolation
|
tinted MudOverlay (Phase 15 §4). MudOverlay — like the former MudPopover — PORTALS its
|
||||||
never reaches the rendered panel — its chrome must live here in the global sheet,
|
content out of the component's DOM subtree, so Blazor CSS isolation never reaches the
|
||||||
not in the scoped WaveformVisualizerControls.razor.css. (The scoped file keeps only
|
rendered panel: its chrome, the three-row/section LAYOUT, the section labels, the slider,
|
||||||
the inline-bar fallback Mix's legacy TopRowCenter mount uses, which is not portaled.)
|
and the toggles all live here in the global sheet, not in the scoped
|
||||||
|
WaveformVisualizerControls.razor.css. (The scoped file keeps only the legacy inline-bar
|
||||||
|
fallback Mix's old TopRowCenter mount used, which is not portaled.)
|
||||||
|
|
||||||
The waveform-visualizer-control-panel class is applied ONLY when the component's
|
The waveform-visualizer-control-panel class is applied ONLY when the component's
|
||||||
PanelChrome="true" parameter is set — which WaveformVisualizerControlPopover does
|
PanelChrome="true" parameter is set — which the popover host does and Mix's inline mount
|
||||||
and Mix's inline mount does NOT. This prevents the chrome from leaking onto Mix's
|
does NOT — so the chrome never leaks onto an inline bar.
|
||||||
inline controls bar.
|
|
||||||
|
|
||||||
The NowPlaying Hero look (§3g): dark-navy ground, green-accent knobs, light icons,
|
CHROME (Phase 15 §5 — NowPlayingCard treatment): SQUARE corners, lighter-navy ground
|
||||||
muted-navy filler — all from the deepdrft-* token source of truth, no hardcoded hex.
|
(navy-mid), a thin LIGHT border (--deepdrft-border-light, the NowPlayingCard 0.12-alpha
|
||||||
The RadialKnob reads --mud-palette-* for its arc/track/center/label colours; we pin
|
light-on-dark idiom as a token). All token-sourced; no hardcoded hex.
|
||||||
those palette vars to the Hero tokens ON THE PANEL so the panel reads the same
|
|
||||||
navy/green/off-white regardless of the page's light/dark theme.
|
COLOUR PRINCIPLE (§5 — green = interactive, light = non-interactive): the RadialKnob reads
|
||||||
|
--mud-palette-* for its arc/pointer/center/label; we pin --mud-palette-primary to the green
|
||||||
|
accent (interactive arcs/pointers) and --mud-palette-text-primary to light. Caption icons and
|
||||||
|
section labels are LIGHT (static). The slider track/thumb and the lamp toggles are green.
|
||||||
============================================================================= */
|
============================================================================= */
|
||||||
.waveform-visualizer-control-panel.mix-visualizer-controls-bar {
|
.waveform-visualizer-control-panel.mix-visualizer-controls-bar {
|
||||||
/* Dark-navy elevated panel ground (§3g: navy-mid for the elevated surface). */
|
/* Lighter-navy elevated panel ground (§5: navy-mid). */
|
||||||
background: var(--deepdrft-navy-mid);
|
background: var(--deepdrft-navy-mid);
|
||||||
border: 1px solid var(--deepdrft-border-green);
|
/* Square corners + thin light border — NowPlayingCard chrome (§5). */
|
||||||
border-radius: 8px;
|
border: 1px solid var(--deepdrft-border-light);
|
||||||
|
border-radius: 0;
|
||||||
|
/* Optional backdrop blur — cheap on a small modal panel, nice over the visualizer (§5). */
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
padding: 1rem 1.25rem;
|
padding: 1rem 1.25rem;
|
||||||
/* Popover panel: cap width so eight 64px knobs wrap to a tidy grid rather than one long bar.
|
/* Three-row sectioned deck: stack the rows top-to-bottom; conditional rows reserve no permanent
|
||||||
This OVERRIDES the inline-bar min-height reserve (which only matters for Mix's non-popover mount). */
|
height (§3 reflow discipline). This OVERRIDES the inline-bar min-height + flex-wrap (which only
|
||||||
|
matter for Mix's non-portaled legacy mount). */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
max-width: 340px;
|
max-width: 420px;
|
||||||
/* Pin the MudBlazor palette vars the portaled RadialKnob consumes to the Hero tokens. */
|
/* Pin the MudBlazor palette vars the portaled RadialKnob + slider consume. */
|
||||||
--mud-palette-primary: var(--deepdrft-green-accent); /* knob value arc / pointer / center stroke */
|
--mud-palette-primary: var(--deepdrft-green-accent); /* knob arc/pointer + slider track/thumb (interactive) */
|
||||||
--mud-palette-surface: var(--deepdrft-navy); /* knob center fill — darkest navy reads against the panel */
|
--mud-palette-surface: var(--deepdrft-navy); /* knob center fill — darkest navy reads against the panel */
|
||||||
--mud-palette-surface-variant: var(--deepdrft-muted); /* knob background track — muted-navy filler (§3g) */
|
--mud-palette-surface-variant: var(--deepdrft-muted); /* knob background track — muted-navy filler */
|
||||||
--mud-palette-text-primary: var(--deepdrft-white); /* knob value label — light (§3g) */
|
--mud-palette-text-primary: var(--deepdrft-white); /* knob value label — light */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Green-accent caption icons (§3g: light/green icons). MudIcon is portaled here too, so this is a
|
/* ── Row layout (§3). Each row is a horizontal band; row 1 + row 3 space-between so the right-pinned
|
||||||
plain global descendant selector — no ::deep, no scope attribute (CSS isolation does not reach
|
control (color / width) hugs the right edge and never reflows when an inner control hides. ── */
|
||||||
inside the popover). */
|
.waveform-visualizer-control-panel .wvc-row {
|
||||||
.waveform-visualizer-control-panel .waveform-visualizer-control-icon {
|
display: flex;
|
||||||
color: var(--deepdrft-green-accent);
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.85rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waveform-visualizer-control-panel .wvc-row-mode,
|
||||||
|
.waveform-visualizer-control-panel .wvc-row-section {
|
||||||
|
/* space-between pushes the right-pinned control to the far edge; the left group holds the rest. */
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The left group of row 1 (toggles + conditional collisions) flows left; the color knob is the
|
||||||
|
space-between right sibling, so it stays put when collisions hides (§3). */
|
||||||
|
.waveform-visualizer-control-panel .wvc-row-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.85rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The right-pinned control (color in row 1, width in row 3) — sits as the space-between right sibling. */
|
||||||
|
.waveform-visualizer-control-panel .wvc-row-right {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Section label "LAVA:" / "WAVE:" (§3, §5). NowPlayingCard .np-label TYPOGRAPHY (mono, uppercase,
|
||||||
|
tracked), recoloured LIGHT — labels are static, so light by the colour principle (§5, §10.3). ── */
|
||||||
|
.waveform-visualizer-control-panel .wvc-section-label {
|
||||||
|
font-family: var(--deepdrft-font-mono);
|
||||||
|
font-size: 0.6rem;
|
||||||
|
letter-spacing: 0.25em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--deepdrft-white);
|
||||||
|
align-self: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── The lamp toggles (§3 row 1). Iconographic lit/unlit lamp glyph, GREEN because interactive (§5).
|
||||||
|
Color="Color.Primary" already drives the glyph currentColor to the pinned green --mud-palette-primary;
|
||||||
|
this just sizes the hit-target to read as a row-1 peer of the knobs. ── */
|
||||||
|
.waveform-visualizer-control-panel .wvc-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── The scroll SLIDER (§8). Track/thumb green (the pinned --mud-palette-primary, interactive). Give it
|
||||||
|
a sensible width so it reads as "position along a continuum" next to the rotary width knob. ── */
|
||||||
|
.waveform-visualizer-control-panel .wvc-slider {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
min-width: 160px;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waveform-visualizer-control-panel .wvc-slider .mud-slider {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Caption icons + section labels render LIGHT (§5/§9 colour principle: static/decorative = light). MudIcon
|
||||||
|
is portaled here too, so this is a plain global descendant selector — no ::deep, no scope attribute (CSS
|
||||||
|
isolation does not reach inside the overlay). The knob arcs/pointers + slider stay green (interactive). */
|
||||||
|
.waveform-visualizer-control-panel .waveform-visualizer-control-icon {
|
||||||
|
color: var(--deepdrft-white);
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── The modal overlay (Phase 15 §4). MudOverlay is already a full-viewport flex scrim that centers its
|
||||||
|
content (.mud-overlay { display:flex; align-items:center; justify-content:center }), which gives the
|
||||||
|
screen-centered panel on every host for free — we do NOT fight that positioning. We only (a) set the
|
||||||
|
mild modal tint from the SINGLE --deepdrft-modal-scrim-alpha token (§10.5, one point of change) and
|
||||||
|
(b) cap the centered content's height so a tall both-on deck scrolls inside the modal rather than
|
||||||
|
overflowing the viewport. The overlay portals to the body, so these are plain global rules (no scope
|
||||||
|
attribute). The doubled .mud-overlay-scrim.mud-overlay-dark selector (0,2,0) outranks MudBlazor's own
|
||||||
|
.mud-overlay-dark (0,1,0), so the tint wins regardless of stylesheet load order. ── */
|
||||||
|
.waveform-visualizer-control-overlay .mud-overlay-scrim.mud-overlay-dark {
|
||||||
|
background-color: rgba(13, 27, 42, var(--deepdrft-modal-scrim-alpha));
|
||||||
|
}
|
||||||
|
|
||||||
|
.waveform-visualizer-control-overlay .mud-overlay-content {
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 419.98px) {
|
@media (max-width: 419.98px) {
|
||||||
.deepdrft-track-detail-meta {
|
.deepdrft-track-detail-meta {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -17,6 +17,12 @@
|
|||||||
--deepdrft-white: #FAFAF8;
|
--deepdrft-white: #FAFAF8;
|
||||||
--deepdrft-border: rgba(13, 27, 42, 0.10);
|
--deepdrft-border: rgba(13, 27, 42, 0.10);
|
||||||
--deepdrft-border-green: rgba(26, 60, 52, 0.20);
|
--deepdrft-border-green: rgba(26, 60, 52, 0.20);
|
||||||
|
/* Thin light-on-dark border, NowPlayingCard spirit (Phase 15 §5). One token instead of scattering
|
||||||
|
the rgba(250,250,248,0.12) literal NowPlayingCard uses inline. */
|
||||||
|
--deepdrft-border-light: rgba(250, 250, 248, 0.12);
|
||||||
|
/* Modal scrim opacity — the SINGLE point of truth for the visualizer-controls overlay tint
|
||||||
|
(Phase 15 §4/§10.5). Mild so the panel reads as modal without a blackout. Change here once. */
|
||||||
|
--deepdrft-modal-scrim-alpha: 0.3;
|
||||||
|
|
||||||
/* Wireframe font stack */
|
/* Wireframe font stack */
|
||||||
--deepdrft-font-display: "Cormorant Garamond", Georgia, serif;
|
--deepdrft-font-display: "Cormorant Garamond", Georgia, serif;
|
||||||
|
|||||||
Reference in New Issue
Block a user