generated from Labyricorn/labyricorn-project-template
Post Step 4 Completion
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,447 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
SciFiAmbientDisplay_v3co.html
|
||||
Version: v3co (co = Claude Opus 5) Base: v2cs Date: 2026-09-04
|
||||
|
||||
CHANGE: Per-theme OBSERVATION layer isolation.
|
||||
v2cs correctly fixed the `.observation-stage svg > rect:first-child` selector that had
|
||||
never matched, but the rule was GLOBAL -- uncovering the shared canvas layer stack in
|
||||
nine themes at once and making every OBSERVATION display read as a variation of one.
|
||||
v3co keeps the correct selector and makes canvas visibility an explicit, per-theme,
|
||||
default-deny declaration: see the OBSERVATION_CANVAS manifest above class
|
||||
ObservationEngine. Rollback point: SciFiAmbientDisplay_v2cs.html (untouched).
|
||||
-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sci-Fi Ambience Generator - Publish Build v14co</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body class="theme-lcars-tng">
|
||||
|
||||
<!-- Header Frame with Interactive Universe Dropdown -->
|
||||
<header class="lcars-header">
|
||||
<div class="universe-selector-container" id="universe-selector-container">
|
||||
<div class="lcars-elbow-left" id="header-universe-btn" title="Click to Change Sci-Fi Universe">
|
||||
<span id="header-universe-label">STARFLIGHT</span> <span style="font-size: 0.8rem; margin-left: 4px;">▼</span>
|
||||
</div>
|
||||
<div class="universe-dropdown-menu" id="universe-dropdown">
|
||||
<!-- Injected dynamically from UniverseRegistry -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lcars-top-bar">
|
||||
<span id="header-ship-name">USS INTERPRISE-G // MAIN BRIDGE</span>
|
||||
<div class="header-volume-dock">
|
||||
<button class="btn-header-mute" id="btn-header-mute" title="Toggle Mute (M)">MUTE</button>
|
||||
<span class="header-vol-val" id="header-vol-val">75%</span>
|
||||
</div>
|
||||
<span id="header-matrix-label">STARFLIGHT SOUND MATRIX v2.9</span>
|
||||
</div>
|
||||
<div class="lcars-pill-end"></div>
|
||||
</header>
|
||||
|
||||
<!-- Main Application Container -->
|
||||
<main class="lcars-container">
|
||||
|
||||
<!-- Left Sidebar: Active Universe Presets (Purely segregated per theme) -->
|
||||
<aside class="left-sidebar">
|
||||
<div class="sidebar-heading" id="sidebar-title">STARSHIP PRESETS</div>
|
||||
<div class="preset-list" id="preset-container">
|
||||
<!-- Dynamically injected preset buttons for active universe only -->
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Center Panel: Master Controls, Presets Description & Channel Synthesizers -->
|
||||
<section class="center-panel">
|
||||
|
||||
<!-- Master Control Card -->
|
||||
<div class="panel-card">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<div class="panel-title" id="current-preset-title">INTERPRISE-G: MAIN BRIDGE</div>
|
||||
<div class="panel-sub" id="current-preset-desc">Warm, low-frequency 50Hz hull tone with gentle ventilation and soft LCARD computer chirps.</div>
|
||||
</div>
|
||||
<div class="diagnostic-stats" style="width: 220px;">
|
||||
<div class="stat-box">
|
||||
<span class="stat-label">AUDIO ENGINE</span>
|
||||
<span class="stat-value" id="stat-audio-status">STANDBY</span>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<span class="stat-label">CORE FREQ</span>
|
||||
<span class="stat-value" id="stat-core-freq">58.0 Hz</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Master Transport & Actions -->
|
||||
<div class="transport-bar">
|
||||
<button class="btn-lcars-large" id="btn-master-play">
|
||||
<span id="play-icon">▶</span> <span id="play-text">ENGAGE</span>
|
||||
</button>
|
||||
|
||||
<div class="pill-button-group" id="universe-events-container">
|
||||
<!-- Dynamically populated per universe -->
|
||||
</div>
|
||||
<div class="watch-experience-wrap">
|
||||
<button class="btn-lcars-pill btn-observation" id="btn-observation" title="Start the full-screen procedural visual and sound experience">WATCH EXPERIENCE</button>
|
||||
<div class="watch-experience-help">Full-screen procedural visuals, soundscape, telemetry, events, and optional local AI announcements.</div>
|
||||
</div>
|
||||
<label class="observation-hud-control" title="Toggle starship viewport window frame (ON: framed starship lounge / OFF: pure deep space)">
|
||||
<span class="observation-hud-label">VIEWPORT</span>
|
||||
<input type="checkbox" id="toggle-observation-viewport" checked aria-label="Toggle Observation Viewport Frame">
|
||||
<span class="observation-hud-state" id="val-observation-viewport">ON</span>
|
||||
</label>
|
||||
<label class="observation-hud-control" title="Toggle vertical structural window pillars / mullions (OFF: unobstructed panoramic view / ON: structural posts)">
|
||||
<span class="observation-hud-label">PILLARS</span>
|
||||
<input type="checkbox" id="toggle-observation-pillars" aria-label="Toggle Window Pillars">
|
||||
<span class="observation-hud-state" id="val-observation-pillars">OFF</span>
|
||||
</label>
|
||||
<div class="observation-activity-control" title="Controls how frequently and intensely Observation generates transient visual activity">
|
||||
<span class="observation-activity-label">ACTIVITY</span>
|
||||
<input type="range" id="slider-observation-activity" min="0" max="100" step="1" value="60" aria-label="Observation activity level">
|
||||
<span class="observation-activity-value" id="val-observation-activity">60%</span>
|
||||
</div>
|
||||
<label class="observation-hud-control" title="Keep OBSERVATION profile/status information visible. Turn off to fade it out over 30 seconds.">
|
||||
<span class="observation-hud-label">HUD HOLD</span>
|
||||
<input type="checkbox" id="toggle-observation-hud" checked aria-label="Keep Observation information visible">
|
||||
<span class="observation-hud-state" id="val-observation-hud">ON</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Dedicated Master Volume Console -->
|
||||
<div class="master-volume-console">
|
||||
<div class="master-vol-top">
|
||||
<div class="master-vol-title-group">
|
||||
<span class="master-vol-tag">MAIN MASTER VOLUME</span>
|
||||
<span class="master-vol-readout" id="val-master-vol">75%</span>
|
||||
<span class="master-vol-db" id="val-master-db">-2.5 dB</span>
|
||||
</div>
|
||||
<div class="master-vol-actions">
|
||||
<button class="btn-lcars-pill btn-mute" id="btn-master-mute" title="Toggle Mute (M)">MUTE</button>
|
||||
<div class="master-vol-presets">
|
||||
<button class="btn-vol-step" data-vol="0.25">25%</button>
|
||||
<button class="btn-vol-step" data-vol="0.50">50%</button>
|
||||
<button class="btn-vol-step active" data-vol="0.75">75%</button>
|
||||
<button class="btn-vol-step" data-vol="1.00">MAX</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="master-slider-row">
|
||||
<button class="vol-adj-btn" id="btn-vol-down" title="Volume Down (-5% or Down Arrow)">−</button>
|
||||
<div class="slider-track-container">
|
||||
<input type="range" id="slider-master-vol" min="0" max="1" step="0.01" value="0.75" class="slider-master-large" aria-label="Main Volume">
|
||||
<!-- Visual 10-Segment LED Level Meter -->
|
||||
<div class="vol-meter-leds" id="vol-meter-leds">
|
||||
<div class="led-pip active"></div>
|
||||
<div class="led-pip active"></div>
|
||||
<div class="led-pip active"></div>
|
||||
<div class="led-pip active"></div>
|
||||
<div class="led-pip active"></div>
|
||||
<div class="led-pip active"></div>
|
||||
<div class="led-pip active"></div>
|
||||
<div class="led-pip active"></div>
|
||||
<div class="led-pip"></div>
|
||||
<div class="led-pip"></div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="vol-adj-btn" id="btn-vol-up" title="Volume Up (+5% or Up Arrow)">+</button>
|
||||
</div>
|
||||
|
||||
<!-- Futuristic Noice Cancellation: deterministic comfort filtering of the three ambience beds -->
|
||||
<div class="fnc-console" id="fnc-console">
|
||||
<div class="fnc-top">
|
||||
<div class="fnc-title-group">
|
||||
<span class="fnc-title">FNC // FUTURISTIC NOICE CANCELLATION</span>
|
||||
<span class="fnc-state" id="fnc-state">MONITORING</span>
|
||||
</div>
|
||||
<button class="btn-lcars-pill btn-fnc enabled" id="btn-fnc-enable" aria-pressed="true" title="Enable or bypass Futuristic Noice Cancellation">FNC ENABLED</button>
|
||||
</div>
|
||||
<div class="fnc-level-row">
|
||||
<span class="fnc-level-label">FNC LEVEL</span>
|
||||
<input type="range" id="slider-fnc-level" min="0" max="1" step="0.01" value="0.65" aria-label="Futuristic Noice Cancellation level">
|
||||
<span class="fnc-level-value" id="val-fnc-level">65%</span>
|
||||
</div>
|
||||
<div class="fnc-detail" id="fnc-detail">Comfort suppression active — Life Support, Hull Drone, and Worp Core are reduced according to FNC level.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Real-Time Frequency Spectrum Visualizer -->
|
||||
<div class="panel-card">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">ACOUSTIC FREQUENCY SPECTRUM</span>
|
||||
<span class="panel-sub">PROCEDURAL HARMONIC ANALYSIS</span>
|
||||
</div>
|
||||
<div class="canvas-wrapper spectrum-wrapper">
|
||||
<div class="canvas-label-overlay">SPECTRUM BIN: 32 CH // REAL-TIME FFT</div>
|
||||
<canvas id="spectrum-canvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4-Channel Synthesizer Mixer Card -->
|
||||
<div class="panel-card">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">PROCEDURAL SOUND ENGINE MIXER</span>
|
||||
<span class="panel-sub">LIVE PARAMETER MODULATION</span>
|
||||
</div>
|
||||
|
||||
<div class="mixer-grid">
|
||||
<!-- Channel 1: Hull Vibration -->
|
||||
<div class="channel-strip">
|
||||
<div class="channel-header">
|
||||
<span>1. HULL DRONE</span>
|
||||
<span id="val-hull-vol">65%</span>
|
||||
</div>
|
||||
<div class="control-row">
|
||||
<div class="control-label"><span>LEVEL</span></div>
|
||||
<input type="range" id="slider-hull-vol" min="0" max="1" step="0.01" value="0.65">
|
||||
</div>
|
||||
<div class="control-row">
|
||||
<div class="control-label"><span>BASE TONE</span><span id="val-hull-freq">50 Hz</span></div>
|
||||
<input type="range" id="slider-hull-freq" min="30" max="120" step="1" value="50">
|
||||
</div>
|
||||
<div class="control-row">
|
||||
<div class="control-label"><span>DAMPING CUTOFF</span><span id="val-hull-cutoff">105 Hz</span></div>
|
||||
<input type="range" id="slider-hull-cutoff" min="50" max="300" step="5" value="105">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Channel 2: Worp Core Pulse -->
|
||||
<div class="channel-strip">
|
||||
<div class="channel-header">
|
||||
<span>2. WORP CORE</span>
|
||||
<span id="val-warp-vol">35%</span>
|
||||
</div>
|
||||
<div class="control-row">
|
||||
<div class="control-label"><span>LEVEL</span></div>
|
||||
<input type="range" id="slider-warp-vol" min="0" max="1" step="0.01" value="0.35">
|
||||
</div>
|
||||
<div class="control-row">
|
||||
<div class="control-label"><span>PULSE RATE (BPM)</span><span id="val-warp-bpm">48 BPM</span></div>
|
||||
<input type="range" id="slider-warp-bpm" min="20" max="120" step="1" value="48">
|
||||
</div>
|
||||
<div class="control-row">
|
||||
<div class="control-label"><span>REACTOR PITCH</span><span id="val-warp-carrier">58 Hz</span></div>
|
||||
<input type="range" id="slider-warp-carrier" min="35" max="150" step="1" value="58">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Channel 3: Life Support -->
|
||||
<div class="channel-strip">
|
||||
<div class="channel-header">
|
||||
<span>3. LIFE SUPPORT</span>
|
||||
<span id="val-air-vol">55%</span>
|
||||
</div>
|
||||
<div class="control-row">
|
||||
<div class="control-label"><span>LEVEL</span></div>
|
||||
<input type="range" id="slider-air-vol" min="0" max="1" step="0.01" value="0.55">
|
||||
</div>
|
||||
<div class="control-row">
|
||||
<div class="control-label"><span>AIRFLOW AIR FILTER</span><span id="val-air-cutoff">1600 Hz</span></div>
|
||||
<input type="range" id="slider-air-cutoff" min="500" max="4000" step="50" value="1600">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Channel 4: Telemetry -->
|
||||
<div class="channel-strip">
|
||||
<div class="channel-header">
|
||||
<span>4. TELEMETRY</span>
|
||||
<span id="val-telemetry-vol">45%</span>
|
||||
</div>
|
||||
<div class="control-row">
|
||||
<div class="control-label"><span>LEVEL</span></div>
|
||||
<input type="range" id="slider-telemetry-vol" min="0" max="1" step="0.01" value="0.45">
|
||||
</div>
|
||||
<div class="control-row">
|
||||
<div class="control-label"><span>BACKGROUND CHIRP DENSITY</span><span id="val-telemetry-density">65%</span></div>
|
||||
<input type="range" id="slider-telemetry-density" min="0" max="1" step="0.05" value="0.65">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<!-- Right Sidebar: Worp Core Intermix Chamber & LCARD Soundboard -->
|
||||
<aside class="right-column">
|
||||
|
||||
<!-- Core / Rotor Visualizer Card -->
|
||||
<div class="panel-card">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title" id="core-visualizer-title">MATTER / ANTIMATTER CORE</span>
|
||||
</div>
|
||||
<div class="canvas-wrapper warp-core-wrapper">
|
||||
<div class="canvas-label-overlay" id="core-visualizer-label">INTERMIX CHAMBER // ACTIVE</div>
|
||||
<canvas id="warp-core-canvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- XZBT Generative Experience / WebLLM + Kokoro. Optional, network-gated: see agents.md. -->
|
||||
<div class="panel-card ai-experience-card" id="ai-experience-card">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">GENERATIVE EXPERIENCE</span>
|
||||
<span class="panel-sub">LOCAL WEBLLM + KOKORO TTS</span>
|
||||
</div>
|
||||
<div class="ai-status-line">
|
||||
<span>LOCAL GENERATIVE SYSTEM</span>
|
||||
<span class="ai-status-badge" id="ai-status-badge">STANDBY</span>
|
||||
</div>
|
||||
<div class="ai-fixed-model">
|
||||
<b>LANGUAGE MODEL</b><br>
|
||||
<span id="ai-model-fixed">Qwen3-1.7B-q4f16_1-MLC</span><br>
|
||||
<small>Automatically prepared when the experience starts. No model selection required.</small>
|
||||
</div>
|
||||
<div class="ai-engine-row" style="margin-top:7px">
|
||||
<div class="ai-engine-chip">WEBLLM <span id="ai-llm-state">OFF</span></div>
|
||||
<div class="ai-engine-chip">KOKORO <span id="ai-tts-state">OFF</span></div>
|
||||
</div>
|
||||
<div class="ai-control-grid">
|
||||
<button class="btn-lcars-pill" id="btn-ai-prepare">PREPARE AI</button>
|
||||
<button class="btn-lcars-pill" id="btn-ai-generate" disabled>GENERATE NOW</button>
|
||||
<label class="ai-toggle-row"><input type="checkbox" id="ai-auto-enabled" checked> AUTO UPDATES</label>
|
||||
<label class="ai-toggle-row"><input type="checkbox" id="ai-speech-enabled" checked> SPEAK</label>
|
||||
<select class="span-2" id="ai-kokoro-voice" aria-label="Kokoro voice">
|
||||
<option value="af_heart">Kokoro voice loads on preparation</option>
|
||||
</select>
|
||||
<div class="span-2">
|
||||
<div class="control-label"><span>VOICE AUTOMATION</span><span id="ai-robot-value">84%</span></div>
|
||||
<input type="range" id="ai-robot-amount" min="0" max="1" step="0.01" value="0.84" aria-label="Robotic voice effect intensity">
|
||||
</div>
|
||||
<div class="span-2">
|
||||
<div class="control-label"><span>AUTO CADENCE</span><span id="ai-cadence-value">90-180 SEC</span></div>
|
||||
<input type="range" id="ai-cadence" min="30" max="240" step="15" value="90">
|
||||
</div>
|
||||
</div>
|
||||
<div class="ai-progress" id="ai-progress">AI and neural voice models load only when needed and are cached by the browser when supported.</div>
|
||||
<div class="ai-experience-output" id="ai-experience-output">GENERATIVE CHANNEL STANDBY</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- TV Presentation -->
|
||||
<div class="panel-card tv-mode-card" id="tv-mode-card">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">TV PRESENTATION</span>
|
||||
<span class="panel-sub">CAST-FRIENDLY 16:9</span>
|
||||
</div>
|
||||
<label class="ai-toggle-row"><input type="checkbox" id="tv-aspect-lock" checked> LOCK EXPERIENCE TO 16:9</label>
|
||||
<div class="control-label"><span>RENDER DENSITY</span><span id="tv-quality-value">1080P TARGET</span></div>
|
||||
<select id="tv-quality">
|
||||
<option value="0.667">720P PERFORMANCE</option>
|
||||
<option value="1" selected>1080P STANDARD</option>
|
||||
<option value="1.333">1440P HIGH</option>
|
||||
<option value="2">2160P ULTRA</option>
|
||||
</select>
|
||||
<div class="ai-progress">The scene keeps a canonical 16:9 layout on phones, PCs, and cast displays. Chrome still negotiates the final Cast stream resolution.</div>
|
||||
</div>
|
||||
|
||||
<!-- Sleep Timer & Auto-Fadeout Card -->
|
||||
<div class="panel-card">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">SLEEP TIMER</span>
|
||||
</div>
|
||||
<div class="timer-readout" id="timer-display">TIMER INACTIVE (CONTINUOUS)</div>
|
||||
<div class="pill-button-group">
|
||||
<button class="btn-lcars-pill btn-timer" data-minutes="15">15M</button>
|
||||
<button class="btn-lcars-pill btn-timer" data-minutes="30">30M</button>
|
||||
<button class="btn-lcars-pill btn-timer" data-minutes="60">60M</button>
|
||||
<button class="btn-lcars-pill btn-timer" data-minutes="0">OFF</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tactical Soundboard & Console Keypad -->
|
||||
<div class="panel-card">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title" id="soundboard-title">LCARD KEYPAD</span>
|
||||
</div>
|
||||
<div class="telemetry-triggers" id="soundboard-container">
|
||||
<!-- Dynamically populated per universe -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
</main>
|
||||
|
||||
|
||||
<!-- OBSERVATION: passive casting / display mode. Click background to return. -->
|
||||
<section class="observation-overlay" id="observation-overlay" aria-hidden="true" aria-label="Watch Experience full-screen display">
|
||||
<!-- Layer 0 & 1: 60fps Deep Celestial Canvas (Parallax 3D Stars, Warp Streaks, Nebulae, Planets, Encounters) -->
|
||||
<canvas id="observation-canvas" class="observation-canvas"></canvas>
|
||||
|
||||
<!-- Legacy / Vector Art Stage (Preserved for compatibility and vector overlays) -->
|
||||
<div class="observation-stage" id="observation-stage"></div>
|
||||
<div class="observation-sim-layer" id="observation-sim-layer"></div>
|
||||
<div class="observation-event-layer" id="observation-event-layer"></div>
|
||||
|
||||
<!-- Layer 2: Viewport Architecture & Glass (Starship Window Frames & Specular Sheen) -->
|
||||
<div class="observation-viewport-frame" id="observation-viewport-frame"></div>
|
||||
<div class="observation-glass-sheen"></div>
|
||||
<div class="observation-scanlines"></div>
|
||||
<div class="observation-vignette"></div>
|
||||
|
||||
<!-- Layer 3: Emergency Alert Environmental Lighting Wash -->
|
||||
<div class="observation-alert-wash" id="observation-alert-wash"></div>
|
||||
|
||||
<!-- Layer 4: Holographic LCARD HUD & Audio Waveform Sill -->
|
||||
<div class="observation-chrome">
|
||||
<div class="observation-ticker" id="observation-ticker">
|
||||
<span class="observation-ticker-label">SYS</span>
|
||||
<div class="observation-ticker-track">
|
||||
<span class="observation-ticker-text" id="observation-ticker-text">STANDBY</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="observation-topline">
|
||||
<div>
|
||||
<div class="observation-mode-label" id="observation-universe">OBSERVATION</div>
|
||||
<div class="observation-profile" id="observation-profile">ACTIVE PROFILE</div>
|
||||
</div>
|
||||
<div class="observation-status" id="observation-status">
|
||||
OBSERVATION ACTIVE<br>
|
||||
PROCEDURAL AUDIO LINK: STANDBY
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Holographic Audio Waveform Sill -->
|
||||
<div class="observation-waveform-container" id="observation-waveform-container">
|
||||
<span class="observation-waveform-label">SUBSPACE HARMONICS</span>
|
||||
<canvas id="observation-waveform-canvas" width="480" height="28"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- In-Observation Quick Glass Control Dock (Auto-hides on inactivity) -->
|
||||
<div class="observation-control-dock" id="observation-control-dock">
|
||||
<button class="obs-dock-btn" id="obs-btn-warp" title="Toggle Warp Flight / Orbital Cruise">WARP: OFF</button>
|
||||
<button class="obs-dock-btn active" id="obs-btn-frame" title="Toggle Starship Viewport Framing">VIEWPORT: ON</button>
|
||||
<button class="obs-dock-btn" id="obs-btn-pillars" title="Toggle Window Pillars / Mullions">PILLARS: OFF</button>
|
||||
<button class="obs-dock-btn" id="obs-btn-alert" title="Toggle Red Alert">RED ALERT</button>
|
||||
<select class="obs-dock-select" id="obs-select-preset" title="Switch Preset"></select>
|
||||
<button class="obs-dock-btn obs-dock-close" id="obs-btn-exit" title="Return to Main Console">RETURN ✕</button>
|
||||
</div>
|
||||
|
||||
<div class="observation-return" id="observation-return-pill">CLICK BACKGROUND TO RETURN</div>
|
||||
</section>
|
||||
|
||||
<!-- LCARD Footer Status -->
|
||||
<footer class="lcars-footer">
|
||||
<span id="footer-status-label">SYSTEM STATUS: NORMAL</span>
|
||||
<span><a href="https://www.labyricorn.com/" target="_blank" rel="noopener noreferrer" style="color:inherit;text-decoration:none;">VIBE ENGINEERED BY LABYRICORN STUDIOS</a></span>
|
||||
<span id="footer-hotkeys-label">SPACE: PLAY/PAUSE | M: MUTE | ↑/↓: VOLUME</span>
|
||||
</footer>
|
||||
|
||||
<!-- All-in-One Procedural Audio Engine & UI -->
|
||||
<script src="js/audio.js"></script>
|
||||
<script src="js/config.js"></script>
|
||||
<script src="js/core-animations.js"></script>
|
||||
<script src="js/visualizer.js"></script>
|
||||
<script src="js/observation-bezels.js"></script>
|
||||
<script src="js/observation-engine.js"></script>
|
||||
<script src="js/control-bus.js"></script>
|
||||
<script src="js/generative-experience.js"></script>
|
||||
<script src="js/contract-adapter.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* XZBT Control Bus
|
||||
*
|
||||
* XZBTControlBus is a small registry of named, settable console targets
|
||||
* (ranges, toggles and triggers). Anything that wants to drive the console
|
||||
* from outside the DOM — contract adapter, hotkeys, future transports —
|
||||
* talks to the bus rather than reaching into app.js internals.
|
||||
*
|
||||
* MIDI NOTE: Web MIDI control belonged to standalone SciFi-XZBT until
|
||||
* Step 3.5. External MIDI integration will be handled by XZBT-NGN, which
|
||||
* will send canonical contract commands (set / invoke) to this exhibit.
|
||||
* The control bus itself is transport-agnostic and is intentionally kept here.
|
||||
*/
|
||||
|
||||
class XZBTControlBus {
|
||||
constructor() {
|
||||
this.targets = new Map();
|
||||
this.listeners = new Set();
|
||||
}
|
||||
register(id, spec) {
|
||||
this.targets.set(id, { id, ...spec });
|
||||
}
|
||||
set(id, value, source = 'external') {
|
||||
const t = this.targets.get(id);
|
||||
if (!t) return false;
|
||||
let v = value;
|
||||
if (t.type === 'range') {
|
||||
v = Math.max(t.min, Math.min(t.max, Number(value)));
|
||||
if (t.step) v = Math.round(v / t.step) * t.step;
|
||||
}
|
||||
if (t.apply) t.apply(v, source);
|
||||
this.listeners.forEach(fn => fn({ id, value: v, source, target: t }));
|
||||
return true;
|
||||
}
|
||||
trigger(id, source = 'external') {
|
||||
const t = this.targets.get(id);
|
||||
if (!t) return false;
|
||||
if (t.apply) t.apply(true, source);
|
||||
this.listeners.forEach(fn => fn({ id, value: true, source, target: t }));
|
||||
return true;
|
||||
}
|
||||
list() {
|
||||
return [...this.targets.values()];
|
||||
}
|
||||
}
|
||||
window.XZBTControlBus = XZBTControlBus;
|
||||
@@ -0,0 +1,216 @@
|
||||
// Approved settings-panel core animations. Procedural, offline, and dependency-free.
|
||||
(() => {
|
||||
'use strict';
|
||||
const TAU = Math.PI * 2;
|
||||
function line(c, points, color, width = 1) {
|
||||
c.beginPath(); points.forEach(([x, y], i) => i ? c.lineTo(x, y) : c.moveTo(x, y));
|
||||
c.strokeStyle = color; c.lineWidth = width; c.stroke();
|
||||
}
|
||||
function ring(c, x, y, r, color, width = 1, start = 0, end = TAU) {
|
||||
c.beginPath(); c.arc(x, y, r, start, end); c.strokeStyle = color; c.lineWidth = width; c.stroke();
|
||||
}
|
||||
function dot(c, x, y, r, color) {
|
||||
c.fillStyle = color; c.beginPath(); c.arc(x, y, r, 0, TAU); c.fill();
|
||||
}
|
||||
function glow(c, x, y, r, color, alpha = 1) {
|
||||
c.save(); c.globalAlpha = alpha;
|
||||
const g = c.createRadialGradient(x, y, 0, x, y, r);
|
||||
g.addColorStop(0, color); g.addColorStop(1, color + '00');
|
||||
c.fillStyle = g; c.fillRect(x - r, y - r, r * 2, r * 2); c.restore();
|
||||
}
|
||||
function rect(c, x, y, w, h, fill, stroke) {
|
||||
c.fillStyle = fill; c.fillRect(x, y, w, h);
|
||||
if (stroke) { c.strokeStyle = stroke; c.lineWidth = 1; c.strokeRect(x, y, w, h); }
|
||||
}
|
||||
function polygon(c, r, sides, color, rotation = 0, width = 1) {
|
||||
const p = Array.from({ length: sides + 1 }, (_, i) => [Math.cos(i * TAU / sides + rotation) * r, Math.sin(i * TAU / sides + rotation) * r]);
|
||||
line(c, p, color, width);
|
||||
}
|
||||
function base(c) {
|
||||
for (let x = -160; x <= 160; x += 20) for (let y = -120; y <= 120; y += 20) dot(c, x, y, 0.6, '#253545');
|
||||
line(c, [[-158, -93], [-158, -110], [-141, -110]], '#344556');
|
||||
line(c, [[158, 93], [158, 110], [141, 110]], '#344556');
|
||||
}
|
||||
|
||||
const renderers = {
|
||||
starfleet(c, t, load) {
|
||||
const beat = Math.pow((Math.sin(t * 3.4) + 1) / 2, 5);
|
||||
glow(c, 0, 0, 92, '#167eca', 0.17 + load * 0.2);
|
||||
rect(c, -39, -105, 78, 210, '#0b1b2b', '#335770');
|
||||
for (const side of [-1, 1]) {
|
||||
line(c, [[side * 34, -105], [side * 48, -78], [side * 48, 78], [side * 34, 105]], '#6f8d9c', 3);
|
||||
rect(c, -37, side < 0 ? -110 : 102, 74, 8, '#b89976');
|
||||
line(c, [[side * 28, 0], [side * 87, 0], [side * 106, 20], [side * 146, 20]], '#234253', 8);
|
||||
line(c, [[side * 28, 0], [side * 87, 0], [side * 106, 20], [side * 146, 20]], '#80b4c4', 1);
|
||||
for (let i = 0; i < 18; i++) {
|
||||
const travel = (i / 18 + t * 0.55) % 1;
|
||||
const y = side * (100 - travel * 97);
|
||||
const x = Math.sin(travel * 13 + t * 2) * 12;
|
||||
line(c, [[x, y + side * 9], [x, y]], side < 0 ? '#75baff' : '#c8f5ff', 1 + load * 2);
|
||||
}
|
||||
}
|
||||
for (let i = -6; i <= 6; i++) {
|
||||
const wave = Math.pow(Math.max(0, Math.cos(t * 3.4 - Math.abs(i) * 0.62)), 6);
|
||||
c.save(); c.globalAlpha = 0.2 + wave * (0.4 + load * 0.4);
|
||||
line(c, [[-32, i * 15], [-22, i * 15 + 3], [22, i * 15 + 3], [32, i * 15]], '#89eaff', 3); c.restore();
|
||||
}
|
||||
glow(c, 0, 0, 28 + beat * 18 * load, '#81ddff', 0.8);
|
||||
line(c, [[0, -20], [14, 0], [0, 20], [-14, 0], [0, -20]], '#e8fdff', 2);
|
||||
line(c, [[0, -20], [0, 20], [14, 0], [-14, 0]], '#b0eeff');
|
||||
},
|
||||
military(c, t, load) {
|
||||
polygon(c, 104, 6, '#3f4a50', Math.PI / 6, 12);
|
||||
polygon(c, 110, 6, '#a7987e', Math.PI / 6);
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const a = i * TAU / 6 + Math.PI / 6;
|
||||
dot(c, Math.cos(a) * 104, Math.sin(a) * 104, 3, '#d6c6ad');
|
||||
c.save(); c.rotate(a);
|
||||
rect(c, 38, -10, 38, 20, '#171f25', '#655e4d');
|
||||
for (let j = 0; j < 5; j++) rect(c, 42 + j * 6, -6, 4, 12, j < 1 + load * 4 && (t * 2 + i) % 6 > j * 0.65 ? '#e8b468' : '#433e30');
|
||||
c.restore();
|
||||
}
|
||||
for (const [r, direction, color] of [[86, 1, '#d3b285'], [31, -1, '#8ea8a6']]) {
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const a = i * TAU / 12 + direction * t * 0.65;
|
||||
ring(c, 0, 0, r, color, 4, a, a + 0.3);
|
||||
}
|
||||
}
|
||||
glow(c, 0, 0, 29, '#eab164', 0.25 + load * 0.4);
|
||||
polygon(c, 18, 6, '#f8d09d', 0, 2);
|
||||
line(c, [[-10, 0], [10, 0]], '#e7d8bf', 3);
|
||||
for (const x of [-136, 126]) for (let j = 0; j < 7; j++) rect(c, x, -38 + j * 12, 10, 7, j < load * 7 ? '#c7a16c' : '#263039');
|
||||
},
|
||||
outlaw(c, t, load) {
|
||||
c.save(); c.translate(Math.sin(t * 19) * load * 1.4, Math.sin(t * 23) * load * 1.5);
|
||||
line(c, [[-126, 50], [-93, 50], [-93, -40], [26, -40], [26, -19]], '#a26c4f', 5);
|
||||
line(c, [[-77, 48], [-54, 66], [47, 66], [66, 29]], '#746554', 4);
|
||||
rect(c, -103, -23, 40, 70, '#302d29', '#81705c');
|
||||
rect(c, -74, -38, 100, 85, '#34383b', '#8b8c7b');
|
||||
rect(c, -53, -45, 29, 17, '#674938', '#c28b58');
|
||||
rect(c, -19, -21, 40, 48, '#233c39', '#609a87');
|
||||
for (let i = 0; i < 4; i++) rect(c, -14, -15 + i * 9, 28, 4, '#79b49e');
|
||||
rect(c, 27, -22, 35, 51, '#494139', '#a08c6e');
|
||||
for (let i = 0; i < 4; i++) rect(c, 30 + i * 8, -27, 4, 61, '#80745f');
|
||||
const flicker = 0.8 + Math.sin(t * 17) * 0.12 + Math.sin(t * 29) * 0.08;
|
||||
c.save(); c.translate(68, 3); c.scale((0.2 + load * 0.85) * flicker, 0.4 + load * 0.5);
|
||||
glow(c, 25, 0, 55, '#e88250', 0.5);
|
||||
for (let j = 0; j < 7; j++) {
|
||||
const len = 42 + 35 * Math.sin(t * 13 + j) ** 2;
|
||||
line(c, [[0, (j - 3) * 4], [len * 0.5, (j - 3) * 3], [len, Math.sin(t * 8 + j) * 5]], j % 2 ? '#ffc07c' : '#a3e5e7', 2);
|
||||
}
|
||||
c.restore();
|
||||
dot(c, -67, 2, 27, '#0b1015'); ring(c, -67, 2, 28, '#b0967a', 3);
|
||||
c.save(); c.translate(-67, 2); c.rotate(t * 4);
|
||||
for (let i = 0; i < 5; i++) { c.rotate(TAU / 5); line(c, [[5, 0], [20, -8], [22, 3], [5, 0]], '#a49c87', 3); }
|
||||
c.restore(); dot(c, -67, 2, 6, '#d8ae7a');
|
||||
c.font = '9px Consolas, monospace'; c.fillStyle = '#dcc1a1'; c.fillText('NO. 07', -15, 43);
|
||||
c.restore();
|
||||
},
|
||||
comedy(c, t, load) {
|
||||
c.translate(0, Math.sin(t * 1.2) * 8);
|
||||
glow(c, 0, 0, 100, '#6b49af', 0.17);
|
||||
const colors = ['#d0acff', '#81ded2', '#f0c78e'];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
c.save(); c.rotate(i * Math.PI / 3 + Math.sin(t * 0.5 + i) * 0.3);
|
||||
const r = 74 + load * 15;
|
||||
const squeeze = 0.22 + Math.abs(Math.sin(t * 0.65 + i)) * 0.55;
|
||||
c.scale(1, squeeze); ring(c, 0, 0, r, colors[i], 2);
|
||||
const a = t * (0.7 + i * 0.2) + i * 2;
|
||||
dot(c, Math.cos(a) * r, Math.sin(a) * r, 5, colors[i]); c.restore();
|
||||
}
|
||||
glow(c, 0, 0, 45, '#a686f4', 0.4);
|
||||
const vertices = Array.from({ length: 6 }, (_, i) => {
|
||||
const a = i * TAU / 6 + t * 0.3;
|
||||
const r = 24 + Math.sin(t * 1.7 + i * 2) * (4 + load * 10);
|
||||
return [Math.cos(a) * r, Math.sin(a) * r];
|
||||
});
|
||||
line(c, [...vertices, vertices[0]], '#ece0ff', 2);
|
||||
for (let i = 0; i < 6; i++) { line(c, [vertices[i], vertices[(i + 2) % 6]], '#b6a4d9'); dot(c, ...vertices[i], 2, '#ffffff'); }
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const a = i * 2.4 + t * 0.18;
|
||||
const x = Math.cos(a) * 121, y = Math.sin(a) * 86;
|
||||
line(c, [[x - 3, y], [x + 3, y]], colors[i % 3]); line(c, [[x, y - 3], [x, y + 3]], colors[i % 3]);
|
||||
}
|
||||
},
|
||||
industrial(c, t, load) {
|
||||
const compression = (Math.sin(t * 2) + 1) / 2;
|
||||
rect(c, -70, -93, 140, 186, '#27241e', '#877653');
|
||||
for (const y of [-94, 85]) for (let i = 0; i < 14; i++) rect(c, -70 + i * 10, y, 10, 9, i % 2 ? '#bd943f' : '#30291a');
|
||||
rect(c, -54, -76, 108, 152, '#0d1011', '#5e543d');
|
||||
glow(c, 0, 0, 60 + compression * load * 18, '#ec8c26', 0.6 + load * 0.3);
|
||||
c.save(); c.scale(1 - compression * 0.25, 1 + compression * 0.13);
|
||||
glow(c, 0, 0, 31 + load * 8, '#ffda87'); c.restore();
|
||||
for (const side of [-1, 1]) {
|
||||
const x = side < 0 ? -133 : 78;
|
||||
rect(c, x, -21, 55, 42, '#2e3332', '#6e7770');
|
||||
const reach = 25 + compression * (12 + load * 16);
|
||||
const inner = side * (77 - reach);
|
||||
rect(c, side < 0 ? -78 : inner, -8, reach, 16, '#a6a092', '#d1c6ac');
|
||||
rect(c, inner - (side < 0 ? 0 : 8), -34, 8, 68, '#746447', '#d9b875');
|
||||
}
|
||||
for (let i = 0; i < 16; i++) {
|
||||
const travel = (i / 16 + t * 0.24) % 1;
|
||||
dot(c, Math.sin(i * 8 + travel * 4) * 36, 60 - travel * 125, 0.8 + (i % 3) * 0.5, '#e4ad61');
|
||||
}
|
||||
for (const y of [-65, 59]) for (let i = 0; i < 8; i++) rect(c, -45 + i * 12, y, 6, 6, i < 2 + load * 6 ? '#e4a751' : '#55422b');
|
||||
for (const x of [-63, 63]) for (const y of [-80, 80]) dot(c, x, y, 3, '#ad9b77');
|
||||
},
|
||||
station(c, t, load) {
|
||||
ring(c, 0, 0, 109, '#34545c');
|
||||
for (let i = 0; i < 4; i++) {
|
||||
c.save(); c.rotate(i * Math.PI / 2);
|
||||
line(c, [[-15, -119], [-15, -106], [15, -106], [15, -119]], '#8ba9a9', 3); c.restore();
|
||||
}
|
||||
c.save(); c.rotate(t * 0.24);
|
||||
ring(c, 0, 0, 89, '#263f44', 19); ring(c, 0, 0, 100, '#93bcb8', 2); ring(c, 0, 0, 78, '#537c7e', 2);
|
||||
for (let i = 0; i < 24; i++) {
|
||||
const a = i * TAU / 24;
|
||||
ring(c, 0, 0, 90, i % 4 === 0 ? '#c4d5bd' : '#568d86', 8, a + 0.025, a + 0.16);
|
||||
}
|
||||
for (let i = 0; i < 6; i++) {
|
||||
c.save(); c.rotate(i * TAU / 6);
|
||||
line(c, [[19, -4], [78, -4]], '#5b797c', 2); line(c, [[19, 4], [78, 4]], '#5b797c', 2);
|
||||
for (let j = 0; j < 1 + Math.floor(load * 3); j++) {
|
||||
const travel = (t * 0.55 + j / 3 + i / 6) % 1;
|
||||
dot(c, 22 + travel * 55, 0, 2, '#b4ffe0');
|
||||
}
|
||||
c.restore();
|
||||
}
|
||||
c.restore();
|
||||
glow(c, 0, 0, 30, '#73e4c2', 0.3 + load * 0.3);
|
||||
dot(c, 0, 0, 21, '#142d32'); ring(c, 0, 0, 21, '#a3d9cd', 2);
|
||||
line(c, [[-10, -5], [10, -5], [10, 5], [-10, 5], [-10, -5]], '#c5e7dc', 2);
|
||||
for (let i = 0; i < 6; i++) ring(c, 0, 0, 115, '#3b625f', 2, i * TAU / 6 + 0.3, i * TAU / 6 + 0.6);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const modes = {
|
||||
'intermix-cascade': 'starfleet',
|
||||
'tactical-flywheel': 'military',
|
||||
'patchwork-drive': 'outlaw',
|
||||
'improbability-engine': 'comedy',
|
||||
'compression-furnace': 'industrial',
|
||||
'station-hub': 'station'
|
||||
};
|
||||
const CoreAnimations = {
|
||||
has(mode) { return Object.prototype.hasOwnProperty.call(modes, mode); },
|
||||
render(ctx, mode, width, height, time, load) {
|
||||
if (!this.has(mode) || width <= 20 || height <= 42) return;
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.save();
|
||||
// Preserve the existing label and the full motion envelope at sidebar size.
|
||||
const labelSpace = 32;
|
||||
const bottomSpace = 10;
|
||||
ctx.translate(width / 2, labelSpace + (height - labelSpace - bottomSpace) / 2);
|
||||
const scale = Math.min((width - 20) / 330, (height - labelSpace - bottomSpace) / 250);
|
||||
ctx.scale(scale, scale);
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
base(ctx);
|
||||
renderers[modes[mode]](ctx, time, Math.max(0, Math.min(1, load)));
|
||||
ctx.restore();
|
||||
}
|
||||
};
|
||||
window.CoreAnimations = CoreAnimations;
|
||||
})();
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* Procedural SVG Viewport Frames & Bezels for Observation Mode
|
||||
*/
|
||||
window.ObservationBezels = {
|
||||
getViewportFrameSvg(universeId, presetId, engine) {
|
||||
if (universeId === 'spacestations') {
|
||||
// Space Stations has its own authentic native station viewport in observationStage!
|
||||
return '';
|
||||
}
|
||||
|
||||
if (universeId === 'starfleet') {
|
||||
let era = 'tng';
|
||||
const preset = (typeof StarshipPresets !== 'undefined' && presetId) ? StarshipPresets[presetId] : null;
|
||||
if (preset && preset.era) era = preset.era;
|
||||
|
||||
if (era === 'tos') {
|
||||
// TOS Original Series - hexagonal bridge viewscreen bezel
|
||||
return `
|
||||
<svg viewBox="0 0 1600 900" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="tosMetal" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#2b2118"/>
|
||||
<stop offset="50%" stop-color="#171310"/>
|
||||
<stop offset="100%" stop-color="#0a0806"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M0,0 H1600 V70 L1350,190 H250 L0,70 Z" fill="url(#tosMetal)" stroke="#f59e0b" stroke-width="3"/>
|
||||
<path d="M0,900 H1600 V830 L1350,710 H250 L0,830 Z" fill="url(#tosMetal)" stroke="#f59e0b" stroke-width="3"/>
|
||||
<path d="M0,0 V900 H130 L170,710 V190 L130,0 Z" fill="url(#tosMetal)" stroke="#f59e0b" stroke-width="3"/>
|
||||
<path d="M1600,0 V900 H1470 L1430,710 V190 L1470,0 Z" fill="url(#tosMetal)" stroke="#f59e0b" stroke-width="3"/>
|
||||
|
||||
${(engine ? engine.showPillars : false) ? `
|
||||
<rect x="530" y="195" width="30" height="515" fill="#171310" stroke="#dc2626" stroke-width="2.5" opacity="0.9"/>
|
||||
<rect x="1040" y="195" width="30" height="515" fill="#171310" stroke="#dc2626" stroke-width="2.5" opacity="0.9"/>
|
||||
` : ''}
|
||||
|
||||
<text x="800" y="865" text-anchor="middle" fill="#f59e0b" font-family="'Share Tech Mono', monospace" font-size="16" letter-spacing="4">U.S.S. INTERPRISE NCC-1701.5 // BRIDGE VIEWSCREEN</text>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
if (era === 'voyager') {
|
||||
// Wayfarer Intrepid Class - sleeker rounded modern arch, cyan accent
|
||||
return `
|
||||
<svg viewBox="0 0 1600 900" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="voyMetal" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#0f2027"/>
|
||||
<stop offset="50%" stop-color="#0a1418"/>
|
||||
<stop offset="100%" stop-color="#050a0c"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M0,0 H1600 V70 Q800,130 0,70 Z" fill="url(#voyMetal)" stroke="#22d3ee" stroke-width="3"/>
|
||||
<path d="M0,900 H1600 V830 Q800,770 0,830 Z" fill="url(#voyMetal)" stroke="#22d3ee" stroke-width="3"/>
|
||||
<path d="M0,0 V900 H80 Q110,450 80,0 Z" fill="url(#voyMetal)"/>
|
||||
<path d="M1600,0 V900 H1520 Q1490,450 1520,0 Z" fill="url(#voyMetal)"/>
|
||||
|
||||
${(engine ? engine.showPillars : false) ? `
|
||||
<path d="M525,90 L535,800 L515,800 L505,90 Z" fill="#0a1418" stroke="#22d3ee" stroke-width="2" opacity="0.9"/>
|
||||
<path d="M1075,90 L1065,800 L1085,800 L1095,90 Z" fill="#0a1418" stroke="#22d3ee" stroke-width="2" opacity="0.9"/>
|
||||
` : ''}
|
||||
|
||||
<g transform="translate(500, 838)">
|
||||
<rect x="0" y="0" width="600" height="20" rx="10" fill="#22d3ee" opacity="0.85"/>
|
||||
<text x="300" y="15" text-anchor="middle" fill="#000" font-family="'Antonio', sans-serif" font-weight="700" font-size="12" letter-spacing="3">USS WAYFARER // BRIDGE VIEWPORT</text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
if (era === 'ds9') {
|
||||
// Defiance Class / DS9 Ops - angular, aggressive warship framing
|
||||
return `
|
||||
<svg viewBox="0 0 1600 900" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="defMetal" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#1c1410"/>
|
||||
<stop offset="50%" stop-color="#0c0806"/>
|
||||
<stop offset="100%" stop-color="#1c1410"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M0,0 H1600 V60 L1300,170 H300 L0,60 Z" fill="url(#defMetal)" stroke="#f97316" stroke-width="3"/>
|
||||
<path d="M0,900 H1600 V840 L1300,730 H300 L0,840 Z" fill="url(#defMetal)" stroke="#f97316" stroke-width="3"/>
|
||||
<path d="M0,0 V900 H150 L190,730 V170 L150,0 Z" fill="url(#defMetal)" stroke="#f97316" stroke-width="3"/>
|
||||
<path d="M1600,0 V900 H1450 L1410,730 V170 L1450,0 Z" fill="url(#defMetal)" stroke="#f97316" stroke-width="3"/>
|
||||
<text x="800" y="865" text-anchor="middle" fill="#f97316" font-family="'Share Tech Mono', monospace" font-size="16" letter-spacing="4">USS DEFIANCE // TACTICAL BRIDGE VIEWPORT</text>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
if (era === 'nx') {
|
||||
// Interprise NS-01 - early, industrial, submarine-like bulkhead
|
||||
return `
|
||||
<svg viewBox="0 0 1600 900" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="nxMetal" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#1c1917"/>
|
||||
<stop offset="50%" stop-color="#0c0a09"/>
|
||||
<stop offset="100%" stop-color="#1c1917"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M0,0 H1600 V110 L1420,175 H180 L0,110 Z" fill="url(#nxMetal)" stroke="#a16207" stroke-width="3"/>
|
||||
<path d="M0,900 H1600 V790 L1420,725 H180 L0,790 Z" fill="url(#nxMetal)" stroke="#a16207" stroke-width="3"/>
|
||||
<path d="M0,0 V900 H120 L150,725 V175 L120,0 Z" fill="url(#nxMetal)" stroke="#a16207" stroke-width="3"/>
|
||||
<path d="M1600,0 V900 H1480 L1450,725 V175 L1480,0 Z" fill="url(#nxMetal)" stroke="#a16207" stroke-width="3"/>
|
||||
<text x="800" y="865" text-anchor="middle" fill="#a16207" font-family="'Share Tech Mono', monospace" font-size="16" letter-spacing="4">INTERPRISE NS-01 // COMMAND BRIDGE VIEWPORT</text>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
// era === 'tng' (or unrecognized) falls through to the default Ten Forward arch below.
|
||||
}
|
||||
|
||||
if (universeId === 'industrial' || universeId === 'outlaw') {
|
||||
// Heavy Industrial Reinforced Bulkhead & Riveted Blast Shutter Framing
|
||||
return `
|
||||
<svg viewBox="0 0 1600 900" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="indMetal" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#1c1917"/>
|
||||
<stop offset="50%" stop-color="#0c0a09"/>
|
||||
<stop offset="100%" stop-color="#1c1917"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Outer Reinforced Rim -->
|
||||
<path d="M0,0 H1600 V100 L1460,160 H140 L0,100 Z" fill="url(#indMetal)" stroke="#b45309" stroke-width="3"/>
|
||||
<path d="M0,900 H1600 V800 L1460,740 H140 L0,800 Z" fill="url(#indMetal)" stroke="#b45309" stroke-width="3"/>
|
||||
<path d="M0,0 V900 H110 L140,740 V160 L110,0 Z" fill="url(#indMetal)" stroke="#b45309" stroke-width="3"/>
|
||||
<path d="M1600,0 V900 H1490 L1460,740 V160 L1490,0 Z" fill="url(#indMetal)" stroke="#b45309" stroke-width="3"/>
|
||||
|
||||
${(engine ? engine.showPillars : false) ? `
|
||||
<!-- Vertical Heavy Structural Mullions -->
|
||||
<rect x="530" y="160" width="34" height="580" fill="#292524" stroke="#78350f" stroke-width="3"/>
|
||||
<rect x="1036" y="160" width="34" height="580" fill="#292524" stroke="#78350f" stroke-width="3"/>
|
||||
` : ''}
|
||||
|
||||
<!-- Hazard Stripes on Lower Sill -->
|
||||
<g opacity="0.6">
|
||||
<rect x="220" y="755" width="1160" height="14" fill="#f59e0b"/>
|
||||
<path d="M230,755 L250,769 M270,755 L290,769 M310,755 L330,769 M350,755 L370,769 M390,755 L410,769 M430,755 L450,769 M470,755 L490,769 M510,755 L530,769" stroke="#000" stroke-width="6"/>
|
||||
</g>
|
||||
<text x="800" y="865" text-anchor="middle" fill="#d97706" font-family="'Share Tech Mono', monospace" font-size="16" letter-spacing="4">HEAVY INDUSTRIAL VIEWPORT // DECK 04 CARGO GANTRY</text>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
if (universeId === 'whataverse') {
|
||||
// Gallifreyan Observatory Roundel Viewing Portal / TARDIX Frame
|
||||
// Console era (classic/revival/modern) tints the rings and label.
|
||||
let era = 'revival';
|
||||
const wUniverse = (typeof UniverseRegistry !== 'undefined') ? UniverseRegistry['whataverse'] : null;
|
||||
const preset = (wUniverse && wUniverse.presets && presetId) ? wUniverse.presets[presetId] : null;
|
||||
if (preset && preset.era) era = preset.era;
|
||||
|
||||
let ringA = '#d4af37', ringB = '#00e5ff', dash = '16 12', labelColor = '#d4af37';
|
||||
let label = 'T.A.R.D.I.X. TEMPORAL VORTEX OBSERVATION PORTAL';
|
||||
if (era === 'classic') {
|
||||
ringA = '#b45309'; ringB = '#f5deb3'; dash = '22 10'; labelColor = '#f5deb3';
|
||||
label = 'TYPE 4D CLASSIC CONSOLE // TEMPORAL VIEWPORT';
|
||||
} else if (era === 'modern') {
|
||||
ringA = '#e2e8f0'; ringB = '#94a3b8'; dash = '4 6'; labelColor = '#e2e8f0';
|
||||
label = 'INFINITE WHITE // TEMPORAL OBSERVATION PORTAL';
|
||||
}
|
||||
|
||||
return `
|
||||
<svg viewBox="0 0 1600 900" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Circular Astrolabe Arch Frame -->
|
||||
<path d="M0,0 H1600 V900 H0 Z M800,450 m-680,0 a680,410 0 1,0 1360,0 a680,410 0 1,0 -1360,0" fill="#030712" fill-rule="evenodd" stroke="${ringB}" stroke-width="4"/>
|
||||
<!-- Gallifreyan Circular Inscriptions -->
|
||||
<ellipse cx="800" cy="450" rx="690" ry="420" fill="none" stroke="${ringA}" stroke-width="3" stroke-dasharray="${dash}"/>
|
||||
<ellipse cx="800" cy="450" rx="715" ry="440" fill="none" stroke="${ringB}" stroke-width="1.5" opacity="0.6"/>
|
||||
<text x="800" y="875" text-anchor="middle" fill="${labelColor}" font-family="'Share Tech Mono', monospace" font-size="17" letter-spacing="4">${label}</text>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
if (universeId === 'deepspace') {
|
||||
// Terok Nor / DS9 Arched Station Viewport overlooking Docking Pylons
|
||||
return `
|
||||
<svg viewBox="0 0 1600 900" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0,0 H1600 V120 Q800,190 0,120 Z" fill="#0d1117" stroke="#38bdf8" stroke-width="3"/>
|
||||
<path d="M0,900 H1600 V780 Q800,720 0,780 Z" fill="#0d1117" stroke="#38bdf8" stroke-width="3"/>
|
||||
<path d="M0,0 V900 H140 L160,780 Q180,450 160,120 L140,0 Z" fill="#0d1117" stroke="#38bdf8" stroke-width="3"/>
|
||||
<path d="M1600,0 V900 H1460 L1440,780 Q1420,450 1440,120 L1460,0 Z" fill="#0d1117" stroke="#38bdf8" stroke-width="3"/>
|
||||
|
||||
${(engine ? engine.showPillars : false) ? `
|
||||
<!-- Exterior Docking Ring Structure visible outside -->
|
||||
<path d="M180,520 L420,410 L440,430 L180,560 Z" fill="#1e293b" opacity="0.6" stroke="#38bdf8" stroke-width="2"/>
|
||||
<path d="M1420,520 L1180,410 L1160,430 L1420,560 Z" fill="#1e293b" opacity="0.6" stroke="#38bdf8" stroke-width="2"/>
|
||||
` : ''}
|
||||
|
||||
<text x="800" y="860" text-anchor="middle" fill="#38bdf8" font-family="'Share Tech Mono', monospace" font-size="16" letter-spacing="3">DEEP SPACE STATION // PROMENADE OBSERVATION DECK</text>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
if (universeId === 'bioships' || universeId === 'retrofuture' || universeId === 'military' || universeId === 'comedy') {
|
||||
// These four universes previously had no case here at all and silently fell through
|
||||
// to the Starflight default below, which hardcodes "USS INTERPRISE-G" into the plaque --
|
||||
// showing the wrong ship name in every one of these themes' Observation view.
|
||||
// Fix: reuse the same bulkhead shape (it already themes correctly via the CSS
|
||||
// accent variables set per universe), but pull the REAL ship/deck name for the
|
||||
// plaque text from this universe's own preset data, same source the header uses.
|
||||
const universe = UniverseRegistry[universeId];
|
||||
const preset = (universe && universe.presets && presetId) ? universe.presets[presetId] : null;
|
||||
const plaqueText = preset
|
||||
? `${preset.name.toUpperCase()} // OBSERVATION DECK`
|
||||
: `${universe ? universe.name : universeId.toUpperCase()} // OBSERVATION DECK`;
|
||||
|
||||
return `
|
||||
<svg viewBox="0 0 1600 900" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="genBulkhead" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#1e222d"/>
|
||||
<stop offset="50%" stop-color="#10141e"/>
|
||||
<stop offset="100%" stop-color="#0a0d14"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M0,0 H1600 V80 Q800,150 0,80 Z" fill="url(#genBulkhead)" stroke="var(--primary-accent, #ff9900)" stroke-width="3"/>
|
||||
<path d="M0,900 H1600 V810 Q800,740 0,810 Z" fill="url(#genBulkhead)" stroke="var(--secondary-accent, #cc99cc)" stroke-width="3"/>
|
||||
<path d="M0,0 V900 H90 Q120,450 90,0 Z" fill="url(#genBulkhead)"/>
|
||||
<path d="M1600,0 V900 H1510 Q1480,450 1510,0 Z" fill="url(#genBulkhead)"/>
|
||||
|
||||
${(engine ? engine.showPillars : false) ? `
|
||||
<path d="M510,95 L535,95 L545,765 L500,765 Z" fill="#151922" stroke="var(--tertiary-accent, #99ccff)" stroke-width="2.5" opacity="0.95"/>
|
||||
<path d="M1090,95 L1065,95 L1055,765 L1100,765 Z" fill="#151922" stroke="var(--tertiary-accent, #99ccff)" stroke-width="2.5" opacity="0.95"/>
|
||||
` : ''}
|
||||
|
||||
<g transform="translate(480, 830)">
|
||||
<rect x="0" y="0" width="640" height="24" rx="12" fill="var(--primary-accent, #ff9900)" opacity="0.85"/>
|
||||
<text x="320" y="17" text-anchor="middle" fill="#000" font-family="'Antonio', sans-serif" font-weight="700" font-size="14" letter-spacing="3">${plaqueText}</text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
// Default: Iconic Starflight Ten Forward / Galaxy-Class Observation Lounge
|
||||
return `
|
||||
<svg viewBox="0 0 1600 900" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="sfBulkhead" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#1e222d"/>
|
||||
<stop offset="50%" stop-color="#10141e"/>
|
||||
<stop offset="100%" stop-color="#0a0d14"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Top Arched Bulkhead Canopy -->
|
||||
<path d="M0,0 H1600 V80 Q800,150 0,80 Z" fill="url(#sfBulkhead)" stroke="var(--primary-accent, #ff9900)" stroke-width="3"/>
|
||||
|
||||
<!-- Bottom Curved Padded Console Sill -->
|
||||
<path d="M0,900 H1600 V810 Q800,740 0,810 Z" fill="url(#sfBulkhead)" stroke="var(--secondary-accent, #cc99cc)" stroke-width="3"/>
|
||||
|
||||
<!-- Side Bulkheads -->
|
||||
<path d="M0,0 V900 H90 Q120,450 90,0 Z" fill="url(#sfBulkhead)"/>
|
||||
<path d="M1600,0 V900 H1510 Q1480,450 1510,0 Z" fill="url(#sfBulkhead)"/>
|
||||
|
||||
${(engine ? engine.showPillars : false) ? `
|
||||
<!-- Vertical Architectural Tapered Mullions dividing into 3 bays -->
|
||||
<path d="M510,95 L535,95 L545,765 L500,765 Z" fill="#151922" stroke="var(--tertiary-accent, #99ccff)" stroke-width="2.5" opacity="0.95"/>
|
||||
<path d="M1090,95 L1065,95 L1055,765 L1100,765 Z" fill="#151922" stroke="var(--tertiary-accent, #99ccff)" stroke-width="2.5" opacity="0.95"/>
|
||||
` : ''}
|
||||
|
||||
<!-- Authentic LCARD Display Bar along lower sill -->
|
||||
<g transform="translate(480, 830)">
|
||||
<rect x="0" y="0" width="640" height="24" rx="12" fill="var(--primary-accent, #ff9900)" opacity="0.85"/>
|
||||
<text x="320" y="17" text-anchor="middle" fill="#000" font-family="'Antonio', sans-serif" font-weight="700" font-size="14" letter-spacing="3">USS INTERPRISE-G // OBSERVATION LOUNGE // DECK 10 FORWARD</text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,673 @@
|
||||
class StarshipVisualizer {
|
||||
constructor(audioManager, warpSynth) {
|
||||
this.am = audioManager;
|
||||
this.warpSynth = warpSynth;
|
||||
|
||||
this.spectrumCanvas = null;
|
||||
this.spectrumCtx = null;
|
||||
this.warpCoreCanvas = null;
|
||||
this.warpCoreCtx = null;
|
||||
|
||||
this.animationFrameId = null;
|
||||
this.pulseEnergy = 0.2;
|
||||
this.warpParticles = [];
|
||||
this.mode = 'warp-core'; // 'warp-core' or 'time-rotor'
|
||||
this.rotorPhase = 0;
|
||||
this.coreTime = 0;
|
||||
this.coreLoad = 0.6;
|
||||
this.lastFrameTime = null;
|
||||
|
||||
// Hook into worp core pulse callback
|
||||
if (this.warpSynth) {
|
||||
this.warpSynth.onPulse = (phase, duration) => {
|
||||
this.pulseEnergy = 1.0;
|
||||
this.spawnWarpPulses();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
setMode(mode) {
|
||||
this.mode = mode || 'warp-core';
|
||||
}
|
||||
|
||||
init(spectrumCanvasId, warpCoreCanvasId) {
|
||||
this.spectrumCanvas = document.getElementById(spectrumCanvasId);
|
||||
if (this.spectrumCanvas) {
|
||||
this.spectrumCtx = this.spectrumCanvas.getContext('2d');
|
||||
}
|
||||
|
||||
this.warpCoreCanvas = document.getElementById(warpCoreCanvasId);
|
||||
if (this.warpCoreCanvas) {
|
||||
this.warpCoreCtx = this.warpCoreCanvas.getContext('2d');
|
||||
this.initWarpParticles();
|
||||
}
|
||||
|
||||
window.addEventListener('resize', () => this.resizeCanvases());
|
||||
this.resizeCanvases();
|
||||
this.startRenderLoop();
|
||||
}
|
||||
|
||||
resizeCanvases() {
|
||||
if (this.spectrumCanvas) {
|
||||
const rect = this.spectrumCanvas.parentElement.getBoundingClientRect();
|
||||
this.spectrumCanvas.width = rect.width * window.devicePixelRatio;
|
||||
this.spectrumCanvas.height = (rect.height || 160) * window.devicePixelRatio;
|
||||
this.spectrumCtx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
||||
}
|
||||
if (this.warpCoreCanvas) {
|
||||
const rect = this.warpCoreCanvas.parentElement.getBoundingClientRect();
|
||||
this.warpCoreCanvas.width = rect.width * window.devicePixelRatio;
|
||||
this.warpCoreCanvas.height = (rect.height || 260) * window.devicePixelRatio;
|
||||
this.warpCoreCtx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
||||
}
|
||||
}
|
||||
|
||||
initWarpParticles() {
|
||||
this.warpParticles = [];
|
||||
for (let i = 0; i < 36; i++) {
|
||||
this.warpParticles.push({
|
||||
y: Math.random(),
|
||||
speed: (Math.random() * 0.008 + 0.004) * (Math.random() > 0.5 ? 1 : -1),
|
||||
size: Math.random() * 4 + 2,
|
||||
opacity: Math.random() * 0.7 + 0.3
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
spawnWarpPulses() {
|
||||
// The new machines render their own flow; only legacy particle modes need a pool.
|
||||
if (this.mode !== 'warp-core' && this.mode !== 'time-rotor') return;
|
||||
for (let i = 0; i < 6; i++) {
|
||||
this.warpParticles.push({
|
||||
y: 0.5, // Center matter/antimatter reaction plane
|
||||
speed: (Math.random() * 0.018 + 0.01) * (i % 2 === 0 ? 1 : -1),
|
||||
size: Math.random() * 6 + 3,
|
||||
opacity: 1.0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
startRenderLoop() {
|
||||
this.lastFrameTime = null;
|
||||
const render = (now) => {
|
||||
const elapsed = this.lastFrameTime === null ? 0 : Math.min((now - this.lastFrameTime) / 1000, 0.05);
|
||||
this.lastFrameTime = now;
|
||||
const volume = this.warpSynth && !this.warpSynth.isMuted ? this.warpSynth.params.volume : 0;
|
||||
const targetLoad = Math.max(0, Math.min(1, volume * (0.65 + this.pulseEnergy * 0.35)));
|
||||
this.coreLoad += (targetLoad - this.coreLoad) * (1 - Math.exp(-elapsed * 6));
|
||||
this.coreTime += elapsed * (this.mode === 'tactical-flywheel' ? 0.6 + this.coreLoad * 0.8 : 1);
|
||||
this.renderSpectrum();
|
||||
if (window.CoreAnimations && CoreAnimations.has(this.mode)) {
|
||||
if (this.warpCoreCanvas && this.warpCoreCtx) {
|
||||
CoreAnimations.render(this.warpCoreCtx, this.mode,
|
||||
this.warpCoreCanvas.width / window.devicePixelRatio,
|
||||
this.warpCoreCanvas.height / window.devicePixelRatio,
|
||||
this.coreTime, this.coreLoad);
|
||||
}
|
||||
} else {
|
||||
switch (this.mode) {
|
||||
case 'time-rotor':
|
||||
this.renderTimeRotor();
|
||||
break;
|
||||
case 'industrial-reactor':
|
||||
this.renderIndustrialReactor();
|
||||
break;
|
||||
case 'bio-heart':
|
||||
this.renderBioHeart();
|
||||
break;
|
||||
case 'retro-oscilloscope':
|
||||
this.renderRetroOscilloscope();
|
||||
break;
|
||||
case 'singularity-core':
|
||||
this.renderSingularityCore();
|
||||
break;
|
||||
case 'warp-core':
|
||||
default:
|
||||
this.renderWarpCore();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Decay pulse energy smoothly
|
||||
this.pulseEnergy = Math.max(0.15, this.pulseEnergy * Math.pow(0.94, elapsed * 60));
|
||||
|
||||
this.animationFrameId = requestAnimationFrame(render);
|
||||
};
|
||||
|
||||
if (this.animationFrameId) cancelAnimationFrame(this.animationFrameId);
|
||||
this.animationFrameId = requestAnimationFrame(render);
|
||||
}
|
||||
|
||||
renderSpectrum() {
|
||||
if (!this.spectrumCanvas || !this.spectrumCtx || !this.am.analyser) return;
|
||||
|
||||
const ctx = this.spectrumCtx;
|
||||
const w = this.spectrumCanvas.width / window.devicePixelRatio;
|
||||
const h = this.spectrumCanvas.height / window.devicePixelRatio;
|
||||
|
||||
const bufferLength = this.am.analyser.frequencyBinCount;
|
||||
const dataArray = new Uint8Array(bufferLength);
|
||||
this.am.analyser.getByteFrequencyData(dataArray);
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
// Dynamic grid color per visualizer mode
|
||||
let gridCol = 'rgba(255, 153, 0, 0.12)';
|
||||
if (this.mode === 'time-rotor' || this.mode === 'singularity-core') gridCol = 'rgba(0, 229, 255, 0.12)';
|
||||
else if (this.mode === 'bio-heart') gridCol = 'rgba(16, 185, 129, 0.12)';
|
||||
else if (this.mode === 'retro-oscilloscope') gridCol = 'rgba(34, 197, 94, 0.15)';
|
||||
else if (this.mode === 'industrial-reactor' || this.mode === 'compression-furnace' || this.mode === 'station-hub') gridCol = 'rgba(245, 158, 11, 0.15)';
|
||||
|
||||
ctx.strokeStyle = gridCol;
|
||||
ctx.lineWidth = 1;
|
||||
for (let y = 20; y < h; y += 30) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(w, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Draw segmented frequency bars
|
||||
const numBars = 32;
|
||||
const barWidth = (w / numBars) - 3;
|
||||
|
||||
for (let i = 0; i < numBars; i++) {
|
||||
const binIdx = Math.floor(Math.pow(i / numBars, 1.8) * (bufferLength * 0.6));
|
||||
const val = dataArray[binIdx] || 0;
|
||||
const barHeight = Math.max(4, (val / 255) * (h - 20));
|
||||
|
||||
const x = i * (barWidth + 3);
|
||||
const y = h - barHeight;
|
||||
|
||||
let col;
|
||||
if (this.mode === 'time-rotor') {
|
||||
if (i < 8) col = '#00e5ff';
|
||||
else if (i < 20) col = '#38bdf8';
|
||||
else if (i < 28) col = '#d4af37';
|
||||
else col = '#ffffff';
|
||||
} else if (this.mode === 'bio-heart') {
|
||||
if (i < 8) col = '#10b981';
|
||||
else if (i < 20) col = '#34d399';
|
||||
else if (i < 28) col = '#a855f7';
|
||||
else col = '#c084fc';
|
||||
} else if (this.mode === 'retro-oscilloscope') {
|
||||
col = i < 28 ? '#22c55e' : '#86efac';
|
||||
} else if (this.mode === 'industrial-reactor' || this.mode === 'compression-furnace' || this.mode === 'station-hub') {
|
||||
if (i < 8) col = '#d97706';
|
||||
else if (i < 20) col = '#f59e0b';
|
||||
else if (i < 28) col = '#fbbf24';
|
||||
else col = '#fef08a';
|
||||
} else if (this.mode === 'singularity-core') {
|
||||
if (i < 8) col = '#4f46e5';
|
||||
else if (i < 20) col = '#6366f1';
|
||||
else if (i < 28) col = '#38bdf8';
|
||||
else col = '#ffffff';
|
||||
} else {
|
||||
// Starflight / Classic LCARD
|
||||
if (i < 8) col = '#ff6600';
|
||||
else if (i < 20) col = '#ff9933';
|
||||
else if (i < 28) col = '#cc99cc';
|
||||
else col = '#99ccff';
|
||||
}
|
||||
|
||||
ctx.fillStyle = col;
|
||||
ctx.shadowColor = col;
|
||||
ctx.shadowBlur = val > 120 ? 8 : 0;
|
||||
ctx.fillRect(x, y, barWidth, barHeight);
|
||||
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(x, y - 2, barWidth, 2);
|
||||
}
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
|
||||
renderWarpCore() {
|
||||
if (!this.warpCoreCanvas || !this.warpCoreCtx) return;
|
||||
|
||||
const ctx = this.warpCoreCtx;
|
||||
const w = this.warpCoreCanvas.width / window.devicePixelRatio;
|
||||
const h = this.warpCoreCanvas.height / window.devicePixelRatio;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
const centerX = w / 2;
|
||||
const chamberWidth = Math.min(70, w * 0.4);
|
||||
|
||||
// 1. Draw outer intermix chamber housing
|
||||
ctx.fillStyle = '#111625';
|
||||
ctx.fillRect(centerX - chamberWidth / 2 - 8, 0, chamberWidth + 16, h);
|
||||
|
||||
// Chamber glass gradient
|
||||
const glassGrad = ctx.createLinearGradient(centerX - chamberWidth / 2, 0, centerX + chamberWidth / 2, 0);
|
||||
glassGrad.addColorStop(0, 'rgba(0, 50, 100, 0.4)');
|
||||
glassGrad.addColorStop(0.5, 'rgba(0, 180, 255, 0.15)');
|
||||
glassGrad.addColorStop(1, 'rgba(0, 50, 100, 0.4)');
|
||||
ctx.fillStyle = glassGrad;
|
||||
ctx.fillRect(centerX - chamberWidth / 2, 0, chamberWidth, h);
|
||||
|
||||
// 2. Matter / Antimatter injectors (Top and Bottom)
|
||||
ctx.fillStyle = '#ff9900';
|
||||
ctx.fillRect(centerX - chamberWidth / 2 - 4, 0, chamberWidth + 8, 12);
|
||||
ctx.fillRect(centerX - chamberWidth / 2 - 4, h - 12, chamberWidth + 8, 12);
|
||||
|
||||
// 3. Central Reaction Intermix Chamber (Center glowing disc)
|
||||
const centerY = h / 2;
|
||||
const glowRadius = 24 + this.pulseEnergy * 28;
|
||||
const coreGlow = ctx.createRadialGradient(centerX, centerY, 2, centerX, centerY, glowRadius);
|
||||
coreGlow.addColorStop(0, '#ffffff');
|
||||
coreGlow.addColorStop(0.3, `rgba(0, 210, 255, ${0.7 + this.pulseEnergy * 0.3})`);
|
||||
coreGlow.addColorStop(0.7, `rgba(0, 100, 255, ${0.4 + this.pulseEnergy * 0.4})`);
|
||||
coreGlow.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
||||
|
||||
ctx.fillStyle = coreGlow;
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, glowRadius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// 4. Segmented Magnetic Constriction Coils (horizontal pulsing rings)
|
||||
const numCoils = 14;
|
||||
for (let i = 0; i < numCoils; i++) {
|
||||
const coilY = (i / (numCoils - 1)) * (h - 30) + 15;
|
||||
const distFromCenter = Math.abs(coilY - centerY) / (h / 2);
|
||||
const coilIntensity = Math.max(0.2, (1.0 - distFromCenter * 0.6) * (0.4 + this.pulseEnergy * 0.6));
|
||||
|
||||
ctx.fillStyle = `rgba(0, 230, 255, ${coilIntensity})`;
|
||||
ctx.shadowColor = '#00e6ff';
|
||||
ctx.shadowBlur = this.pulseEnergy > 0.6 ? 12 : 3;
|
||||
|
||||
// Draw coil bar
|
||||
ctx.fillRect(centerX - chamberWidth / 2 + 4, coilY - 2, chamberWidth - 8, 4);
|
||||
}
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// 5. Plasma stream particles
|
||||
for (let i = this.warpParticles.length - 1; i >= 0; i--) {
|
||||
const p = this.warpParticles[i];
|
||||
p.y += p.speed;
|
||||
|
||||
if (p.y < 0 || p.y > 1) {
|
||||
if (this.warpParticles.length > 36) {
|
||||
this.warpParticles.splice(i, 1);
|
||||
continue;
|
||||
} else {
|
||||
p.y = p.speed > 0 ? 0 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
const py = p.y * h;
|
||||
const px = centerX + (Math.sin(p.y * 12) * (chamberWidth * 0.25));
|
||||
|
||||
ctx.fillStyle = `rgba(180, 240, 255, ${p.opacity * (0.4 + this.pulseEnergy * 0.6)})`;
|
||||
ctx.beginPath();
|
||||
ctx.arc(px, py, p.size * (0.8 + this.pulseEnergy * 0.4), 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the canonical TARDIX Central Time Rotor
|
||||
* A glass cylinder containing an interior mechanical column physically rising and falling
|
||||
* in sync with the pulse cycle, illuminated with glowing Gallifreyan cyan/emerald light.
|
||||
*/
|
||||
renderTimeRotor() {
|
||||
if (!this.warpCoreCanvas || !this.warpCoreCtx) return;
|
||||
|
||||
const ctx = this.warpCoreCtx;
|
||||
const w = this.warpCoreCanvas.width / window.devicePixelRatio;
|
||||
const h = this.warpCoreCanvas.height / window.devicePixelRatio;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
const centerX = w / 2;
|
||||
const columnWidth = Math.min(84, w * 0.45);
|
||||
|
||||
// 1. TARDIX Console Plinth & Ceiling Collar (Victorian Brass / Gallifreyan Bronze)
|
||||
const collarGrad = ctx.createLinearGradient(centerX - columnWidth / 2, 0, centerX + columnWidth / 2, 0);
|
||||
collarGrad.addColorStop(0, '#593e10');
|
||||
collarGrad.addColorStop(0.3, '#d4af37');
|
||||
collarGrad.addColorStop(0.7, '#fef08a');
|
||||
collarGrad.addColorStop(1, '#593e10');
|
||||
|
||||
ctx.fillStyle = collarGrad;
|
||||
ctx.fillRect(centerX - columnWidth / 2 - 8, 0, columnWidth + 16, 14);
|
||||
ctx.fillRect(centerX - columnWidth / 2 - 8, h - 14, columnWidth + 16, 14);
|
||||
|
||||
// 2. Outer Glass Column Tube
|
||||
const glassGrad = ctx.createLinearGradient(centerX - columnWidth / 2, 0, centerX + columnWidth / 2, 0);
|
||||
glassGrad.addColorStop(0, 'rgba(0, 40, 80, 0.45)');
|
||||
glassGrad.addColorStop(0.15, 'rgba(0, 229, 255, 0.25)');
|
||||
glassGrad.addColorStop(0.5, 'rgba(255, 255, 255, 0.12)');
|
||||
glassGrad.addColorStop(0.85, 'rgba(0, 229, 255, 0.25)');
|
||||
glassGrad.addColorStop(1, 'rgba(0, 40, 80, 0.45)');
|
||||
|
||||
ctx.fillStyle = glassGrad;
|
||||
ctx.fillRect(centerX - columnWidth / 2, 14, columnWidth, h - 28);
|
||||
|
||||
// Glass edge highlights
|
||||
ctx.strokeStyle = 'rgba(0, 229, 255, 0.6)';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.strokeRect(centerX - columnWidth / 2, 14, columnWidth, h - 28);
|
||||
|
||||
// 3. Central Bobbing Time Rotor Column
|
||||
// Physical oscillation: rises and falls smoothly
|
||||
this.rotorPhase += 0.038;
|
||||
const maxTravel = (h - 90) * 0.35;
|
||||
const rotorOffset = Math.sin(this.rotorPhase) * maxTravel;
|
||||
const rotorCenterY = (h / 2) + rotorOffset;
|
||||
const rotorHeight = (h - 28) * 0.48;
|
||||
|
||||
// Moving Inner Rotor Rod & Glass Tubes
|
||||
const innerWidth = columnWidth * 0.58;
|
||||
|
||||
// Glowing core glow
|
||||
const coreGlow = ctx.createRadialGradient(centerX, rotorCenterY, 4, centerX, rotorCenterY, 36 + this.pulseEnergy * 30);
|
||||
coreGlow.addColorStop(0, '#ffffff');
|
||||
coreGlow.addColorStop(0.4, `rgba(0, 229, 255, ${0.7 + this.pulseEnergy * 0.3})`);
|
||||
coreGlow.addColorStop(0.8, `rgba(0, 100, 200, ${0.3 + this.pulseEnergy * 0.4})`);
|
||||
coreGlow.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
||||
|
||||
ctx.fillStyle = coreGlow;
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, rotorCenterY, 36 + this.pulseEnergy * 30, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Inner mechanical tubes
|
||||
ctx.fillStyle = '#00e5ff';
|
||||
ctx.shadowColor = '#00e5ff';
|
||||
ctx.shadowBlur = 10 + this.pulseEnergy * 10;
|
||||
ctx.fillRect(centerX - 4, rotorCenterY - rotorHeight / 2, 8, rotorHeight);
|
||||
|
||||
// Left and right secondary crystal tubes
|
||||
ctx.fillStyle = 'rgba(180, 240, 255, 0.85)';
|
||||
ctx.fillRect(centerX - innerWidth / 2 + 2, rotorCenterY - rotorHeight / 2 + 10, 5, rotorHeight - 20);
|
||||
ctx.fillRect(centerX + innerWidth / 2 - 7, rotorCenterY - rotorHeight / 2 + 10, 5, rotorHeight - 20);
|
||||
|
||||
// Gallifreyan Circular Rotor Rings
|
||||
for (let r = 0; r < 4; r++) {
|
||||
const ringY = rotorCenterY - rotorHeight / 2 + (r * (rotorHeight / 3));
|
||||
ctx.strokeStyle = '#d4af37';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(centerX, ringY, innerWidth / 2 + 2, 5, 0, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// 4. Sparkling Vortex Time Energy Particles
|
||||
for (let i = this.warpParticles.length - 1; i >= 0; i--) {
|
||||
const p = this.warpParticles[i];
|
||||
p.y += p.speed * 0.8;
|
||||
|
||||
if (p.y < 0.05 || p.y > 0.95) {
|
||||
if (this.warpParticles.length > 36) {
|
||||
this.warpParticles.splice(i, 1);
|
||||
continue;
|
||||
} else {
|
||||
p.y = p.speed > 0 ? 0.05 : 0.95;
|
||||
}
|
||||
}
|
||||
|
||||
const py = p.y * h;
|
||||
const px = centerX + (Math.sin(p.y * 16 + this.rotorPhase) * (columnWidth * 0.32));
|
||||
|
||||
ctx.fillStyle = `rgba(0, 229, 255, ${p.opacity * (0.5 + this.pulseEnergy * 0.5)})`;
|
||||
ctx.shadowColor = '#00e5ff';
|
||||
ctx.shadowBlur = 6;
|
||||
ctx.beginPath();
|
||||
ctx.arc(px, py, p.size * (0.7 + this.pulseEnergy * 0.5), 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Industrial Fusion Reactor (Nostromo, Serenity, Rocinante)
|
||||
* Heavy containment walls, incandescent glowing amber plasma core, heat radiating coils
|
||||
*/
|
||||
renderIndustrialReactor() {
|
||||
if (!this.warpCoreCanvas || !this.warpCoreCtx) return;
|
||||
const ctx = this.warpCoreCtx;
|
||||
const w = this.warpCoreCanvas.width / window.devicePixelRatio;
|
||||
const h = this.warpCoreCanvas.height / window.devicePixelRatio;
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
const centerX = w / 2;
|
||||
const centerY = h / 2;
|
||||
const chamberW = Math.min(80, w * 0.42);
|
||||
|
||||
// Cast iron frame
|
||||
ctx.fillStyle = '#1c150c';
|
||||
ctx.fillRect(centerX - chamberW / 2 - 10, 0, chamberW + 20, h);
|
||||
|
||||
// Hazard warning bands at top and bottom
|
||||
for (let x = centerX - chamberW / 2 - 10; x < centerX + chamberW / 2 + 10; x += 12) {
|
||||
ctx.fillStyle = (x % 24 === 0) ? '#d97706' : '#1a1106';
|
||||
ctx.fillRect(x, 0, 12, 10);
|
||||
ctx.fillRect(x, h - 10, 12, 10);
|
||||
}
|
||||
|
||||
// Incandescent molten amber core
|
||||
const radius = 22 + this.pulseEnergy * 32;
|
||||
const glow = ctx.createRadialGradient(centerX, centerY, 2, centerX, centerY, radius);
|
||||
glow.addColorStop(0, '#ffffff');
|
||||
glow.addColorStop(0.2, '#fef08a');
|
||||
glow.addColorStop(0.5, `rgba(245, 158, 11, ${0.7 + this.pulseEnergy * 0.3})`);
|
||||
glow.addColorStop(1, 'rgba(180, 83, 9, 0)');
|
||||
|
||||
ctx.fillStyle = glow;
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Heat induction coil clamps
|
||||
for (let i = 0; i < 9; i++) {
|
||||
const cy = 20 + i * ((h - 40) / 8);
|
||||
ctx.fillStyle = (i % 2 === 0) ? '#f59e0b' : '#78350f';
|
||||
ctx.shadowColor = '#f59e0b';
|
||||
ctx.shadowBlur = this.pulseEnergy > 0.6 ? 10 : 2;
|
||||
ctx.fillRect(centerX - chamberW / 2, cy - 3, chamberW, 6);
|
||||
}
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Living Leviathon Bio-Heart (Moya, Lexx, Species 8675309)
|
||||
* Pulsing vascular heart sac with bioluminescent emerald/violet energy and neural veins
|
||||
*/
|
||||
renderBioHeart() {
|
||||
if (!this.warpCoreCanvas || !this.warpCoreCtx) return;
|
||||
const ctx = this.warpCoreCtx;
|
||||
const w = this.warpCoreCanvas.width / window.devicePixelRatio;
|
||||
const h = this.warpCoreCanvas.height / window.devicePixelRatio;
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
const centerX = w / 2;
|
||||
const centerY = h / 2;
|
||||
|
||||
// Organic vascular expansion
|
||||
const bioScale = 1.0 + Math.sin(this.rotorPhase * 1.2) * 0.12 + this.pulseEnergy * 0.18;
|
||||
const baseR = 35 * bioScale;
|
||||
|
||||
// Outer bioluminescent aura
|
||||
const aura = ctx.createRadialGradient(centerX, centerY, 4, centerX, centerY, baseR * 1.8);
|
||||
aura.addColorStop(0, '#a7f3d0');
|
||||
aura.addColorStop(0.3, `rgba(16, 185, 129, ${0.7 + this.pulseEnergy * 0.3})`);
|
||||
aura.addColorStop(0.7, `rgba(139, 92, 246, ${0.3 + this.pulseEnergy * 0.3})`);
|
||||
aura.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
||||
|
||||
ctx.fillStyle = aura;
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, baseR * 1.8, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Pulsing neural veins
|
||||
ctx.strokeStyle = '#34d399';
|
||||
ctx.lineWidth = 2.5;
|
||||
ctx.shadowColor = '#10b981';
|
||||
ctx.shadowBlur = 8;
|
||||
for (let v = 0; v < 6; v++) {
|
||||
const angle = (v / 6) * Math.PI * 2 + this.rotorPhase * 0.2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(centerX, centerY);
|
||||
const cpX = centerX + Math.cos(angle + 0.5) * (baseR * 0.8);
|
||||
const cpY = centerY + Math.sin(angle + 0.5) * (baseR * 0.8);
|
||||
const endX = centerX + Math.cos(angle) * (baseR * 1.5);
|
||||
const endY = centerY + Math.sin(angle) * (baseR * 1.5);
|
||||
ctx.quadraticCurveTo(cpX, cpY, endX, endY);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retro Oscilloscope & Analog Astrogator (Jupiter 2, Discovery One)
|
||||
* 1950s/60s green phosphor CRT screen with glowing Lissajous audio wave rings
|
||||
*/
|
||||
renderRetroOscilloscope() {
|
||||
if (!this.warpCoreCanvas || !this.warpCoreCtx) return;
|
||||
const ctx = this.warpCoreCtx;
|
||||
const w = this.warpCoreCanvas.width / window.devicePixelRatio;
|
||||
const h = this.warpCoreCanvas.height / window.devicePixelRatio;
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
const centerX = w / 2;
|
||||
const centerY = h / 2;
|
||||
const crtRadius = Math.min(w, h) * 0.42;
|
||||
|
||||
// Circular CRT bezel
|
||||
ctx.fillStyle = '#052e16';
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, crtRadius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#22c55e';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
|
||||
// Crosshairs
|
||||
ctx.strokeStyle = 'rgba(34, 197, 94, 0.25)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(centerX - crtRadius, centerY);
|
||||
ctx.lineTo(centerX + crtRadius, centerY);
|
||||
ctx.moveTo(centerX, centerY - crtRadius);
|
||||
ctx.lineTo(centerX, centerY + crtRadius);
|
||||
ctx.stroke();
|
||||
|
||||
// Draw a neutral trace before audio initialization, replacing the previous theme.
|
||||
const analyser = this.am.analyser;
|
||||
const bufferLength = analyser ? analyser.fftSize : 128;
|
||||
const dataArray = new Uint8Array(bufferLength);
|
||||
if (analyser) analyser.getByteTimeDomainData(dataArray);
|
||||
else dataArray.fill(128);
|
||||
|
||||
ctx.strokeStyle = '#86efac';
|
||||
ctx.shadowColor = '#22c55e';
|
||||
ctx.shadowBlur = 8;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
|
||||
const points = 48;
|
||||
for (let i = 0; i < points; i++) {
|
||||
const idx = Math.floor((i / points) * (bufferLength / 2));
|
||||
const v = (dataArray[idx] / 128.0) - 1.0;
|
||||
const angle = (i / points) * Math.PI * 2 + this.coreTime;
|
||||
const r = (crtRadius * 0.65) + (v * 28 * (0.8 + this.pulseEnergy));
|
||||
const x = centerX + Math.cos(angle) * r;
|
||||
const y = centerY + Math.sin(angle) * r;
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
// A sweep marker makes rotation legible even when the audio trace is silent.
|
||||
const sweepRadius = (crtRadius * 0.65) + ((dataArray[0] / 128.0) - 1.0) * 28 * (0.8 + this.pulseEnergy);
|
||||
ctx.fillStyle = '#d1fae5';
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX + Math.cos(this.coreTime) * sweepRadius,
|
||||
centerY + Math.sin(this.coreTime) * sweepRadius, 2.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gravity Singularity Core (Event Horizon, Deep Space)
|
||||
* Black hole event horizon with warping gravitational accretion disk
|
||||
*/
|
||||
renderSingularityCore() {
|
||||
if (!this.warpCoreCanvas || !this.warpCoreCtx) return;
|
||||
const ctx = this.warpCoreCtx;
|
||||
const w = this.warpCoreCanvas.width / window.devicePixelRatio;
|
||||
const h = this.warpCoreCanvas.height / window.devicePixelRatio;
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
const centerX = w / 2;
|
||||
const centerY = h / 2;
|
||||
const diskR = Math.min(w, h) * 0.44;
|
||||
|
||||
// Glowing gravitational accretion disk
|
||||
ctx.save();
|
||||
ctx.translate(centerX, centerY);
|
||||
ctx.rotate(this.rotorPhase * 0.6);
|
||||
|
||||
const grad = ctx.createRadialGradient(0, 0, 12, 0, 0, diskR);
|
||||
grad.addColorStop(0, '#000000');
|
||||
grad.addColorStop(0.35, '#000000');
|
||||
grad.addColorStop(0.45, `rgba(99, 102, 241, ${0.8 + this.pulseEnergy * 0.2})`);
|
||||
grad.addColorStop(0.7, `rgba(56, 189, 248, ${0.4 + this.pulseEnergy * 0.3})`);
|
||||
grad.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
||||
|
||||
ctx.fillStyle = grad;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 0, diskR, diskR * 0.35, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
|
||||
// Pure black event horizon sphere at center
|
||||
ctx.fillStyle = '#000000';
|
||||
ctx.strokeStyle = 'rgba(99, 102, 241, 0.8)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.shadowColor = '#6366f1';
|
||||
ctx.shadowBlur = 12 + this.pulseEnergy * 10;
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, 18, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
}
|
||||
|
||||
window.StarshipVisualizer = StarshipVisualizer;
|
||||
|
||||
// Helper: Hex color to RGBA
|
||||
function hexToRgba(hex, alpha = 1) {
|
||||
if (!hex || hex.charAt(0) !== '#') return `rgba(56, 189, 248, ${alpha})`;
|
||||
let c = hex.substring(1);
|
||||
if (c.length === 3) c = c.split('').map(x => x + x).join('');
|
||||
const num = parseInt(c, 16);
|
||||
return `rgba(${(num >> 16) & 255}, ${(num >> 8) & 255}, ${num & 255}, ${alpha})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* ============================================================================
|
||||
* CINEMATIC OBSERVATION LOUNGE ENGINE (v9g)
|
||||
* ============================================================================
|
||||
* Features:
|
||||
* - 60fps DPI-Aware Deep Celestial Canvas (Parallax 3D Starfield & Warp Tunnel)
|
||||
* - Relativistic Warp Flight vs. Orbital Cruise Impulse Modes
|
||||
* - Procedural Celestial Bodies (Class-M Planet with Atmospheric Glow, Time Vortex, Gas Giants)
|
||||
* - Living Traffic & Encounters (Shuttles, Cruisers, Decloaking Klingon BOP, TARDIX, Freighters)
|
||||
* - Viewport Window Framing Architecture per Universe (Starflight, Industrial, Station, Whataverse, Military)
|
||||
* - Emergency Alert Synchronization (Red/Yellow Alert Klaxon Strobes & Shield Grids)
|
||||
* - Subspace Audio Harmonics Waveform Sill
|
||||
* - Auto-Hiding Interactive Glass Control Dock
|
||||
*/
|
||||
// =========================================================================
|
||||
// OBSERVATION CANVAS MANIFEST (v3co)
|
||||
// =========================================================================
|
||||
// Each universe declares exactly which canvas layers it draws. Default-deny:
|
||||
// anything not listed is OFF. A universe missing from this table gets no canvas at all.
|
||||
// This table is the contract that keeps each theme's OBSERVATION its own experience --
|
||||
// do not add a layer here to "fill space"; give the theme its own bespoke content instead.
|
||||
//
|
||||
// `starfield` entries may carry a per-universe profile so that two universes drawing stars
|
||||
// are still drawing THEIR OWN stars (density, palette, scale), not one shared layer.
|
||||
Reference in New Issue
Block a user