Files
deepdrft/DeepDrftPublic.Client/Services/WaveformVisualizerControlState.cs
T
daniel-c-harvey fa01b9c8e0 feat(public): add Theater Mode to release detail pages
Toggle left of the lava popover hides release content so the visualizer fills
the surface; player bar grows to carry the playing release's cover, title, and
share. State on WaveformVisualizerControlState; pages and bar observe it.
2026-06-20 21:51:30 -04:00

166 lines
9.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
namespace DeepDrftPublic.Client.Services;
/// <summary>
/// Holds the waveform visualizer's eight continuous-control positions plus two subsystem on/off
/// 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
/// 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).
///
/// One state object, eight properties — not eight sibling holders, and (deliberately) NO constructor
/// parameters: this is a plain scoped value holder, so widening it (the Phase 10 split of the single
/// density knob into fluid-amount + fluid-viscosity) adds fields + defaults only and never forces a
/// consumer constructor to grow. Each C#-side default mirrors a TS-side tuning anchor in
/// WaveformVisualizer.ts; keep the two in sync, as the <c>DefaultVisibleSeconds</c> /
/// <c>DEFAULT_VISIBLE_SECONDS</c> pair does.
///
/// <para>
/// <see cref="Changed"/> is the decoupling seam between the controls bar and the visualizer bridge.
/// The controls component (a sibling of the backdrop in the page tree) only mutates this shared state
/// and raises <see cref="Changed"/>; the bridge component (<c>WaveformVisualizer</c>) subscribes
/// and pushes the affected uniform/dial to the JS module. This keeps the JS module handle single-owned
/// by the bridge — no handle sharing, no service-locator, no cross-component interop.
/// </para>
/// </summary>
public sealed class WaveformVisualizerControlState
{
// ── The eight control defaults (Phase 10). Each mirrors a DEFAULT_* anchor in
// WaveformVisualizer.ts; keep the two in sync, as the default-sync discipline requires.
// Feel-anchors only — Daniel tunes on screen; the ~20% gravity / ~100% heat pair is his stated
// sweet spot (§4c).
/// <summary>
/// Default scroll-speed dial. Normalized [0,1] → mapped C#-side to a visible time-span in seconds
/// via <see cref="WaveformZoomMapping"/>, then sent to WaveformVisualizer.ts as a seconds value via
/// <c>setScrollSpeed</c>. The TS-side anchor is <c>DEFAULT_VISIBLE_SECONDS</c>. Opens mid-range
/// (0 = slow/wide window, 1 = fast/tight window).
/// </summary>
public const double DefaultScrollSpeed = 0.5;
/// <summary>
/// Default gradient-rotation-speed dial. Mirrors <c>DEFAULT_GRADIENT_ROTATION_SPEED</c> in
/// WaveformVisualizer.ts. Normalized [0,1] → slow→fast anchor-rotation; drives the live OKLab
/// three-colour gradient. 0.45 opens with a clearly-visible ~7 s colour cycle (Phase 10 §3.2).
/// </summary>
public const double DefaultGradientRotationSpeed = 0.45;
/// <summary>
/// Default lava-gravity dial. Mirrors <c>DEFAULT_LAVA_GRAVITY</c> in WaveformVisualizer.ts. Normalized
/// [0,1]; 0 = near-weightless float, 1 = wax falls + settles fast. Tuned to Daniel's ~20% sweet spot.
/// </summary>
public const double DefaultLavaGravity = 0.2;
/// <summary>
/// Default lava-heat dial. Mirrors <c>DEFAULT_LAVA_HEAT</c> in WaveformVisualizer.ts. Normalized [0,1];
/// 0 = wax rests at the bottom (collision-only), 1 = many small turbulent rising bubbles. Tuned to
/// Daniel's ~100% sweet spot.
/// </summary>
public const double DefaultLavaHeat = 1.0;
/// <summary>
/// Default fluid-amount dial. Mirrors <c>DEFAULT_FLUID_AMOUNT</c> in WaveformVisualizer.ts. The first
/// half of the Phase 10 "bubbles" split. Normalized [0,1]; 0 = few small blobs, 1 = many larger
/// blobs (more wax in the container — blob count + per-blob volume).
/// </summary>
public const double DefaultFluidAmount = 0.4;
/// <summary>
/// Default fluid-viscosity / cohesion dial. Mirrors <c>DEFAULT_FLUID_VISCOSITY</c> in
/// WaveformVisualizer.ts. The second half of the Phase 10 "bubbles" split. Normalized [0,1]; 1 = high
/// cohesion (crisp spheres that snap back), 0 = low cohesion (deforms freely, stays gooey/merged).
/// </summary>
public const double DefaultFluidViscosity = 0.6;
/// <summary>
/// Default collision-strength dial. Mirrors <c>DEFAULT_COLLISION_STRENGTH</c> in WaveformVisualizer.ts.
/// Normalized [0,1]; 0 = soft mush, 1 = hard elastic up-and-out throw.
/// </summary>
public const double DefaultCollisionStrength = 0.5;
/// <summary>
/// Default waveform-width dial. Mirrors <c>DEFAULT_WAVEFORM_WIDTH</c> in WaveformVisualizer.ts.
/// Normalized [0,1], mapped onto the useful 10%95% ribbon-extent band (Phase 10 §3.7); 0.5 opens
/// mid-band. Narrowing clears room for the lava.
/// </summary>
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>
/// Default Theater-mode state. <c>false</c> so a fresh page load opens with the full release page,
/// not the bare visualizer (Phase 20 §4/OQ5). Has no TS-side anchor: Theater Mode is a page-chrome
/// presentation flag, not a visualizer dial — the bridge never reads it.
/// </summary>
public const bool DefaultTheaterMode = false;
/// <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>
public double ScrollSpeed { get; set; } = DefaultScrollSpeed;
/// <summary>Gradient anchor-rotation rate, normalized [0,1]. Drives the live OKLab gradient.</summary>
public double GradientRotationSpeed { get; set; } = DefaultGradientRotationSpeed;
/// <summary>Downward force on the wax, normalized [0,1].</summary>
public double LavaGravity { get; set; } = DefaultLavaGravity;
/// <summary>Energy into the lava system, normalized [0,1]. 0 = rest-at-bottom, 1 = roiling.</summary>
public double LavaHeat { get; set; } = DefaultLavaHeat;
/// <summary>Amount of wax (blob count + per-blob volume), normalized [0,1]. Phase 10 split, part 1.</summary>
public double FluidAmount { get; set; } = DefaultFluidAmount;
/// <summary>Fluid viscosity / cohesion, normalized [0,1]. 1 = crisp spheres, 0 = gooey/deformed.
/// Phase 10 split, part 2.</summary>
public double FluidViscosity { get; set; } = DefaultFluidViscosity;
/// <summary>Collision hardness, normalized [0,1]. 0 = soft mush, 1 = hard up-and-out throw.</summary>
public double CollisionStrength { get; set; } = DefaultCollisionStrength;
/// <summary>Waveform-band horizontal extent, normalized [0,1]. Narrowing clears room for the lava.</summary>
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>
/// Whether Theater Mode is on (Phase 20). When <c>true</c> the three release-detail pages remove
/// their release content via <c>@if</c> so the visualizer fills the surface, and the player bar
/// grows to carry the playing release's identity. Distinct from the visualizer dials: the bridge
/// ignores it — the pages and the player bar observe it through the same <see cref="Changed"/> seam.
/// Gated for visibility on <see cref="LavaEnabled"/> || <see cref="WaveformEnabled"/> at the toggle.
/// </summary>
public bool TheaterMode { get; set; } = DefaultTheaterMode;
/// <summary>
/// Raised whenever any control value changes. The visualizer bridge subscribes to push the
/// affected dial(s); the Theater-Mode observers (detail pages, player bar) subscribe to react to
/// <see cref="TheaterMode"/>. Mutators set the property then raise this; subscribers re-read the values.
/// </summary>
public event Action? Changed;
/// <summary>Raise <see cref="Changed"/>. Called by the controls component after mutating a value.</summary>
public void NotifyChanged() => Changed?.Invoke();
}