Initial commit
This commit is contained in:
@@ -1,2 +1,40 @@
|
||||
# SciFi-XZBT
|
||||
# SciFi-XZBT Modular Architecture & Repackaging
|
||||
|
||||
## Directory Overview
|
||||
|
||||
```text
|
||||
SciFi-XZBT/
|
||||
├── index.html # Clean entry point (368 lines)
|
||||
├── css/
|
||||
│ └── style.css # LCARS styling, CRT effects, theme colors (2,317 lines)
|
||||
├── js/
|
||||
│ ├── audio.js # Web Audio synthesis, oscillators, sound engines (2,258 lines)
|
||||
│ ├── config.js # Starship presets, Universe registry & palettes (1,039 lines)
|
||||
│ ├── visualizer.js # Audio spectrum & Warp Core canvas rendering (643 lines)
|
||||
│ ├── observation-bezels.js # Procedural SVG Viewport bezels (TOS, Voyager, TNG, etc.) (271 lines)
|
||||
│ ├── observation-engine.js # Full observation mode canvas and simulation engine (2,784 lines)
|
||||
│ └── app.js # DOM bindings, hotkeys, UI controllers & loop (3,962 lines)
|
||||
├── dist/
|
||||
│ └── SciFiAmbientDisplay_v4.html # Standalone, single-file offline build
|
||||
└── tools/
|
||||
├── package.ps1 # 1-click script to build the standalone single-file HTML
|
||||
└── extract.ps1 # Original extraction script for reference
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Developing Modularly**:
|
||||
- Edit the specific CSS or JS files directly.
|
||||
- For audio or synthesizer logic, focus exclusively on `js/audio.js`.
|
||||
- For viewscreen bezels and framing, edit `js/observation-bezels.js`.
|
||||
- For observation celestial mechanics and canvases, edit `js/observation-engine.js`.
|
||||
- For presets and sound matrix tables, edit `js/config.js`.
|
||||
|
||||
2. **Repackaging to Single-File**:
|
||||
Run the packaging script from PowerShell:
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\tools\package.ps1
|
||||
```
|
||||
This immediately produces `dist\SciFiAmbientDisplay_v4.html`, combining all CSS and JS back into a self-contained, double-clickable offline file.
|
||||
|
||||
+2317
File diff suppressed because it is too large
Load Diff
Vendored
+13645
File diff suppressed because it is too large
Load Diff
+367
@@ -0,0 +1,367 @@
|
||||
<!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 v13co</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">STARFLEET</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 ENTERPRISE // 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">STARFLEET SOUND MATRIX v2.5</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">ENTERPRISE-D: MAIN BRIDGE</div>
|
||||
<div class="panel-sub" id="current-preset-desc">Warm, low-frequency 50Hz hull tone with gentle ventilation and soft LCARS 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 AMBIENCE</span>
|
||||
</button>
|
||||
|
||||
<div class="pill-button-group" id="universe-events-container">
|
||||
<!-- Dynamically populated per universe -->
|
||||
</div>
|
||||
<button class="btn-lcars-pill btn-observation" id="btn-observation" title="Enter passive full-screen observation display">
|
||||
OBSERVATION
|
||||
</button>
|
||||
<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>
|
||||
</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: Warp Core Pulse -->
|
||||
<div class="channel-strip">
|
||||
<div class="channel-header">
|
||||
<span>2. WARP 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: Warp Core Intermix Chamber & LCARS 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>
|
||||
|
||||
<!-- 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">LCARS 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="Observation 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 LCARS 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>
|
||||
|
||||
<!-- LCARS 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/visualizer.js"></script>
|
||||
<script src="js/observation-bezels.js"></script>
|
||||
<script src="js/observation-engine.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+2258
File diff suppressed because it is too large
Load Diff
+1039
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. ENTERPRISE NCC-1701 // BRIDGE VIEWSCREEN</text>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
if (era === 'voyager') {
|
||||
// Voyager 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 VOYAGER // BRIDGE VIEWPORT</text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
if (era === 'ds9') {
|
||||
// Defiant 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 DEFIANT // TACTICAL BRIDGE VIEWPORT</text>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
if (era === 'nx') {
|
||||
// Enterprise NX-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">ENTERPRISE NX-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 === 'whoniverse') {
|
||||
// Gallifreyan Observatory Roundel Viewing Portal / TARDIS Frame
|
||||
// Console era (classic/revival/modern) tints the rings and label.
|
||||
let era = 'revival';
|
||||
const wUniverse = (typeof UniverseRegistry !== 'undefined') ? UniverseRegistry['whoniverse'] : 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.S. TEMPORAL VORTEX OBSERVATION PORTAL';
|
||||
if (era === 'classic') {
|
||||
ringA = '#b45309'; ringB = '#f5deb3'; dash = '22 10'; labelColor = '#f5deb3';
|
||||
label = 'TYPE 40 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 Starfleet default below, which hardcodes "USS ENTERPRISE" 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 Starfleet 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 LCARS 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 ENTERPRISE // OBSERVATION LOUNGE // DECK 10 FORWARD</text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,643 @@
|
||||
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;
|
||||
|
||||
// Hook into warp 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() {
|
||||
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() {
|
||||
const render = () => {
|
||||
this.renderSpectrum();
|
||||
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 * 0.94);
|
||||
|
||||
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') 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') {
|
||||
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 {
|
||||
// Starfleet / Classic LCARS
|
||||
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 TARDIS 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. TARDIS 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 Leviathan Bio-Heart (Moya, Lexx, Species 8472)
|
||||
* 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 || !this.am.analyser) 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();
|
||||
|
||||
// Lissajous audio waveform
|
||||
const bufferLength = this.am.analyser.fftSize;
|
||||
const dataArray = new Uint8Array(bufferLength);
|
||||
this.am.analyser.getByteTimeDomainData(dataArray);
|
||||
|
||||
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.rotorPhase;
|
||||
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();
|
||||
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, TARDIS, Freighters)
|
||||
* - Viewport Window Framing Architecture per Universe (Starfleet, Industrial, Station, Whoniverse, 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.
|
||||
@@ -0,0 +1,80 @@
|
||||
$base = "g:\.vibe\SciFiSS\SciFiAmbientDisplay_v3co.html"
|
||||
$dest = "g:\.vibe\SciFiSS\SciFi-XZBT"
|
||||
$lines = [System.IO.File]::ReadAllLines($base)
|
||||
|
||||
# 1. CSS: lines 19 to 2335 (0-based)
|
||||
[System.IO.File]::WriteAllLines("$dest\css\style.css", $lines[19..2335])
|
||||
|
||||
# 2. HTML: Head (0..18), link css, Body (2338..2676), script tags, Tail (13628..13629)
|
||||
$html = [System.Collections.Generic.List[string]]::new()
|
||||
for ($i = 0; $i -le 17; $i++) { $html.Add($lines[$i]) }
|
||||
$html.Add(' <link rel="stylesheet" href="css/style.css">')
|
||||
$html.Add('</head>')
|
||||
for ($i = 2338; $i -le 2676; $i++) { $html.Add($lines[$i]) }
|
||||
|
||||
$scripts = @(
|
||||
'js/audio.js',
|
||||
'js/config.js',
|
||||
'js/visualizer.js',
|
||||
'js/observation-bezels.js',
|
||||
'js/observation-engine.js',
|
||||
'js/app.js'
|
||||
)
|
||||
foreach ($s in $scripts) {
|
||||
$html.Add(" <script src=`"$s`"></script>")
|
||||
}
|
||||
$html.Add($lines[13628])
|
||||
$html.Add($lines[13629])
|
||||
[System.IO.File]::WriteAllLines("$dest\index.html", $html)
|
||||
|
||||
# 3. js/audio.js: 2683 to 4940
|
||||
[System.IO.File]::WriteAllLines("$dest\js\audio.js", $lines[2683..4940])
|
||||
|
||||
# 4. js/config.js: 4941 to 5979
|
||||
[System.IO.File]::WriteAllLines("$dest\js\config.js", $lines[4941..5979])
|
||||
|
||||
# 5. js/visualizer.js: 5980 to 6622
|
||||
[System.IO.File]::WriteAllLines("$dest\js\visualizer.js", $lines[5980..6622])
|
||||
|
||||
# 6. Viewport bezels extracted into js/observation-bezels.js
|
||||
# In lines 9397..9661: getViewportFrameSvg
|
||||
$bezels = [System.Collections.Generic.List[string]]::new()
|
||||
$bezels.Add('/**')
|
||||
$bezels.Add(' * Procedural SVG Viewport Frames & Bezels for Observation Mode')
|
||||
$bezels.Add(' */')
|
||||
$bezels.Add('window.ObservationBezels = {')
|
||||
$bezels.Add(' getViewportFrameSvg(universeId, presetId, engine) {')
|
||||
for ($i = 9398; $i -le 9660; $i++) {
|
||||
$l = $lines[$i]
|
||||
# replace "this.showPillars" with "(engine ? engine.showPillars : false)"
|
||||
$l = $l -replace 'this\.showPillars', '(engine ? engine.showPillars : false)'
|
||||
$bezels.Add(' ' + $l)
|
||||
}
|
||||
$bezels.Add(' }')
|
||||
$bezels.Add('};')
|
||||
[System.IO.File]::WriteAllLines("$dest\js\observation-bezels.js", $bezels)
|
||||
|
||||
# 7. js/observation-engine.js: 6623 to 9396, delegate getViewportFrameSvg, then lines 9662..9664
|
||||
$obs = [System.Collections.Generic.List[string]]::new()
|
||||
for ($i = 6623; $i -le 9396; $i++) {
|
||||
$obs.Add($lines[$i])
|
||||
}
|
||||
$obs.Add(' getViewportFrameSvg(universeId, presetId) {')
|
||||
$obs.Add(' if (window.ObservationBezels && typeof window.ObservationBezels.getViewportFrameSvg === "function") {')
|
||||
$obs.Add(' return window.ObservationBezels.getViewportFrameSvg(universeId, presetId, this);')
|
||||
$obs.Add(' }')
|
||||
$obs.Add(' return "";')
|
||||
$obs.Add(' }')
|
||||
for ($i = 9662; $i -le 9664; $i++) {
|
||||
$obs.Add($lines[$i])
|
||||
}
|
||||
[System.IO.File]::WriteAllLines("$dest\js\observation-engine.js", $obs)
|
||||
|
||||
# 8. js/app.js: 9666 to 13626
|
||||
$app = [System.Collections.Generic.List[string]]::new()
|
||||
for ($i = 9666; $i -le 13626; $i++) {
|
||||
$app.Add($lines[$i])
|
||||
}
|
||||
[System.IO.File]::WriteAllLines("$dest\js\app.js", $app)
|
||||
|
||||
Write-Output "Extraction complete!"
|
||||
@@ -0,0 +1,77 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Packages the modular SciFi-XZBT project into a single, standalone, offline HTML file.
|
||||
.EXAMPLE
|
||||
.\package.ps1
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$OutputFile = ""
|
||||
)
|
||||
|
||||
$rootDir = (Resolve-Path "$PSScriptRoot\..").Path
|
||||
$indexFile = Join-Path $rootDir "index.html"
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($OutputFile)) {
|
||||
$OutputFile = Join-Path $rootDir "dist\SciFiAmbientDisplay_v4.html"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $indexFile)) {
|
||||
Write-Error "index.html not found in $rootDir"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Packaging SciFi Ambient Display from $rootDir..." -ForegroundColor Cyan
|
||||
|
||||
$indexLines = [System.IO.File]::ReadAllLines($indexFile)
|
||||
$output = [System.Collections.Generic.List[string]]::new()
|
||||
|
||||
foreach ($line in $indexLines) {
|
||||
# Inline CSS stylesheet
|
||||
if ($line -match '<link\s+rel="stylesheet"\s+href="([^"]+)"\s*/?>') {
|
||||
$cssRel = $matches[1]
|
||||
$cssPath = Join-Path $rootDir $cssRel
|
||||
if (Test-Path $cssPath) {
|
||||
Write-Host " -> Inlining CSS: $cssRel" -ForegroundColor Green
|
||||
$output.Add(" <style>")
|
||||
$cssContent = [System.IO.File]::ReadAllLines($cssPath)
|
||||
foreach ($cl in $cssContent) { $output.Add($cl) }
|
||||
$output.Add(" </style>")
|
||||
} else {
|
||||
Write-Warning "CSS file not found: $cssPath"
|
||||
$output.Add($line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
# Inline JS scripts
|
||||
if ($line -match '<script\s+src="([^"]+)"></script>') {
|
||||
$jsRel = $matches[1]
|
||||
$jsPath = Join-Path $rootDir $jsRel
|
||||
if (Test-Path $jsPath) {
|
||||
Write-Host " -> Inlining JS: $jsRel" -ForegroundColor Green
|
||||
$output.Add(" <script>")
|
||||
$jsContent = [System.IO.File]::ReadAllLines($jsPath)
|
||||
foreach ($jl in $jsContent) { $output.Add($jl) }
|
||||
$output.Add(" </script>")
|
||||
} else {
|
||||
Write-Warning "JS file not found: $jsPath"
|
||||
$output.Add($line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
$output.Add($line)
|
||||
}
|
||||
|
||||
$outDir = [System.IO.Path]::GetDirectoryName($OutputFile)
|
||||
if (-not (Test-Path $outDir)) {
|
||||
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
|
||||
}
|
||||
|
||||
[System.IO.File]::WriteAllLines($OutputFile, $output)
|
||||
$fileSize = (Get-Item $OutputFile).Length
|
||||
$fileSizeKb = [math]::Round($fileSize / 1KB, 2)
|
||||
|
||||
Write-Host "Build Successful!" -ForegroundColor Green
|
||||
Write-Host "Output: $OutputFile ($fileSizeKb KB, $($output.Count) lines)" -ForegroundColor Yellow
|
||||
Reference in New Issue
Block a user