namespace DeepDrftPublic.Client.Services; /// /// 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 DefaultVisibleSeconds / /// DEFAULT_VISIBLE_SECONDS pair does. /// /// /// 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 ; the bridge component (WaveformVisualizer) 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. /// /// 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). /// /// Default scroll-speed dial. Normalized [0,1] → mapped C#-side to a visible time-span in seconds /// via , then sent to WaveformVisualizer.ts as a seconds value via /// setScrollSpeed. The TS-side anchor is DEFAULT_VISIBLE_SECONDS. Opens mid-range /// (0 = slow/wide window, 1 = fast/tight window). /// public const double DefaultScrollSpeed = 0.5; /// /// Default gradient-rotation-speed dial. Mirrors DEFAULT_GRADIENT_ROTATION_SPEED 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). /// public const double DefaultGradientRotationSpeed = 0.45; /// /// Default lava-gravity dial. Mirrors DEFAULT_LAVA_GRAVITY in WaveformVisualizer.ts. Normalized /// [0,1]; 0 = near-weightless float, 1 = wax falls + settles fast. Tuned to Daniel's ~20% sweet spot. /// public const double DefaultLavaGravity = 0.2; /// /// Default lava-heat dial. Mirrors DEFAULT_LAVA_HEAT 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. /// public const double DefaultLavaHeat = 1.0; /// /// Default fluid-amount dial. Mirrors DEFAULT_FLUID_AMOUNT 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). /// public const double DefaultFluidAmount = 0.4; /// /// Default fluid-viscosity / cohesion dial. Mirrors DEFAULT_FLUID_VISCOSITY 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). /// public const double DefaultFluidViscosity = 0.6; /// /// Default collision-strength dial. Mirrors DEFAULT_COLLISION_STRENGTH in WaveformVisualizer.ts. /// Normalized [0,1]; 0 = soft mush, 1 = hard elastic up-and-out throw. /// public const double DefaultCollisionStrength = 0.5; /// /// Default waveform-width dial. Mirrors DEFAULT_WAVEFORM_WIDTH 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. /// public const double DefaultWaveformWidth = 0.5; /// /// Default lava-subsystem on-state. true 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. /// public const bool DefaultLavaEnabled = true; /// /// Default waveform-subsystem on-state. true so the waveform ribbon is on out of the box. /// Backs the row-1 waveform lamp toggle (Phase 15 §6). /// public const bool DefaultWaveformEnabled = true; /// /// Default Theater-mode state. false 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. /// public const bool DefaultTheaterMode = false; /// Apparent bottom-to-top scroll rate, normalized [0,1]. Bridge maps it to a visible /// time-span via ; the standalone resolution/zoom control is gone. public double ScrollSpeed { get; set; } = DefaultScrollSpeed; /// Gradient anchor-rotation rate, normalized [0,1]. Drives the live OKLab gradient. public double GradientRotationSpeed { get; set; } = DefaultGradientRotationSpeed; /// Downward force on the wax, normalized [0,1]. public double LavaGravity { get; set; } = DefaultLavaGravity; /// Energy into the lava system, normalized [0,1]. 0 = rest-at-bottom, 1 = roiling. public double LavaHeat { get; set; } = DefaultLavaHeat; /// Amount of wax (blob count + per-blob volume), normalized [0,1]. Phase 10 split, part 1. public double FluidAmount { get; set; } = DefaultFluidAmount; /// Fluid viscosity / cohesion, normalized [0,1]. 1 = crisp spheres, 0 = gooey/deformed. /// Phase 10 split, part 2. public double FluidViscosity { get; set; } = DefaultFluidViscosity; /// Collision hardness, normalized [0,1]. 0 = soft mush, 1 = hard up-and-out throw. public double CollisionStrength { get; set; } = DefaultCollisionStrength; /// Waveform-band horizontal extent, normalized [0,1]. Narrowing clears room for the lava. public double WaveformWidth { get; set; } = DefaultWaveformWidth; /// /// Whether the lava field is drawn. When false 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). /// public bool LavaEnabled { get; set; } = DefaultLavaEnabled; /// /// Whether the waveform ribbon is drawn. When false 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). /// public bool WaveformEnabled { get; set; } = DefaultWaveformEnabled; /// /// Whether Theater Mode is on (Phase 20). When true the three release-detail pages remove /// their release content via @if 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 seam. /// Gated for visibility on || at the toggle. /// public bool TheaterMode { get; set; } = DefaultTheaterMode; /// /// 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 /// . Mutators set the property then raise this; subscribers re-read the values. /// public event Action? Changed; // Whether the one-time, capability-driven default has been applied this session. The default-set // (lava off when the browser has no WebGL hardware acceleration) must run exactly once — on the // first interactive render, before the listener has touched a toggle — so it sets the *initial // default* and never clobbers a later explicit in-session toggle. Scoped with the rest of this // state, so it survives SPA navigation (a remounted visualizer does not re-apply) and resets on a // fresh page load (F5 re-probes). private bool _capabilityDefaultApplied; /// /// Applies the hardware-capability default exactly once per session: when the browser reports no /// WebGL hardware acceleration, the lava subsystem (the expensive, main-thread software-rendered /// part that starves audio decode) defaults off while the waveform stays on. With /// acceleration present this is a no-op — lava keeps its on-state. /// /// /// Idempotent and guarded: only the FIRST call this session has any effect, so it sets the initial /// default and never overrides a listener's explicit toggle (the control remains fully functional — /// a user on a software renderer may re-enable lava at their own risk). Mutates, coerces /// Theater Mode, then raises once so the controls UI, the visualizer bridge, /// and the Theater observers all reflect the default in a single cycle. Called by the visualizer /// bridge on first interactive render, once JS interop (the probe) is available. /// /// /// /// The probe result — true when WebGL hardware acceleration is present (or the renderer is /// unknown/masked, favoring the common case), false only on a positive software-renderer /// match or total WebGL failure. /// public void ApplyCapabilityDefault(bool hardwareAccelerated) { if (_capabilityDefaultApplied) return; _capabilityDefaultApplied = true; // Accelerated (or unknown): keep the as-shipped defaults — no observer churn. if (hardwareAccelerated) return; LavaEnabled = false; // the expensive subsystem off; WaveformEnabled stays at its default (true). CoerceTheaterMode(); NotifyChanged(); } /// /// Enforces the Theater-Mode invariant: Theater Mode cannot remain on when both visualizer /// subsystems are off (there is nothing to go to theater FOR). Call this after mutating /// or and before /// so all observers see a consistent, coerced state in the same /// cycle. /// public void CoerceTheaterMode() { if (TheaterMode && !LavaEnabled && !WaveformEnabled) TheaterMode = false; } /// Raise . Called by the controls component after mutating a value. public void NotifyChanged() => Changed?.Invoke(); }