Checkpoint ambient display, generative experience, and MIDI work
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
# SciFi Ambient Display
|
||||
|
||||
A browser-based sci-fi ambience generator. It layers procedurally synthesized starship drones, warp/worp core pulses, life-support hiss, and background telemetry chirps into a continuous soundscape, paired with a matching on-screen console and an optional full-screen "observation deck" visualizer. Good for background noise while working, relaxing, or falling asleep.
|
||||
A browser-based sci-fi ambience generator. It layers procedurally synthesized starship drones, warp/worp core pulses, life-support hiss, and background telemetry chirps into a continuous soundscape, paired with a matching on-screen console and an optional full-screen "observation deck" visualizer, with optional local AI announcements and MIDI control. Good for background noise while working, relaxing, or falling asleep.
|
||||
|
||||
## Getting Started
|
||||
|
||||
The simplest way to use it: open `dist/SciFiAmbientDisplay_v4.html` in any modern browser (double-click it — no install, no server, no internet connection required). It's a single self-contained file with everything bundled in.
|
||||
The simplest way to use it: open the newest `dist/SciFiAmbientDisplay_V*.html` in any modern browser (double-click it — no install, no server, no internet connection required). It's a single self-contained file with everything bundled in.
|
||||
|
||||
If you'd rather run it from the source files instead, open `index.html` the same way, or serve the folder with any static file server.
|
||||
|
||||
@@ -38,13 +38,41 @@ The mixer has four independent channels, each with its own volume:
|
||||
|
||||
Mute everything instantly with the header mute button, or use the master volume control. The soundboard (bottom right) adds one-off sound effects specific to the current universe — chimes, alerts, door sounds, and the like — layered on top of the ambient mix.
|
||||
|
||||
## Futuristic Noice Cancellation
|
||||
|
||||
FNC is a comfort filter for the three continuous beds. It's on by default, and the FNC LEVEL slider decides how hard it pulls them down:
|
||||
|
||||
| Channel | Reduction across the slider's range |
|
||||
| --- | --- |
|
||||
| Life Support | 50% → 100% |
|
||||
| Hull Drone | 10% → 70% |
|
||||
| Worp Core | 5% → 50% |
|
||||
|
||||
The point is to keep the room tone present without letting it sit on top of whatever else you're listening to. FNC steps aside on its own during alerts, universe events, and soundboard hits, then eases back in — the readout tells you which state it's in. Turn it off entirely with the FNC ENABLED button and the channels play at their raw mixer levels.
|
||||
|
||||
## Generative Experience (optional)
|
||||
|
||||
The GENERATIVE EXPERIENCE panel adds spoken ship's-computer announcements written in the voice of the current universe — clinical and precise on a Starflight bridge, eccentric and put-upon on a Whataverse console. Press PREPARE AI to download the local language model and neural voice; both then run entirely in your browser, and the browser caches them for later sessions.
|
||||
|
||||
This is the one part of the app that needs the internet, and only on that first preparation. It also needs a WebGPU-capable browser (current Chrome or Edge). Without either, the panel reports it and everything else works exactly as before.
|
||||
|
||||
Turn AUTO UPDATES off if you'd rather trigger announcements yourself, SPEAK off for text without voice, and use VOICE AUTOMATION to dial in how synthetic the voice sounds — each universe has its own default.
|
||||
|
||||
## MIDI Control
|
||||
|
||||
If you have a MIDI controller, press ENABLE MIDI and the browser will ask for access. Pick a target from the dropdown, press MIDI LEARN, then move the knob, fader or pad you want bound to it. Volume, every channel parameter, FNC level, activity, and the transport and alert buttons are all mappable. Bindings are saved in the browser and survive a reload; CLEAR MAP wipes them.
|
||||
|
||||
## TV Presentation
|
||||
|
||||
The TV PRESENTATION panel locks the Watch Experience to a 16:9 frame with black bars, so the scene looks the same whether it's on a phone, a monitor, or a cast display — useful when sending it to a TV via Chrome. RENDER DENSITY trades detail against performance from 720p up to 2160p; drop it if the animation stutters on a weaker machine.
|
||||
|
||||
## Sleep Timer
|
||||
|
||||
Set the ambience to automatically fade out and stop after 15, 30, or 60 minutes — handy for falling asleep to. Choose "OFF" to run continuously.
|
||||
|
||||
## Observation Mode
|
||||
## Watch Experience
|
||||
|
||||
Click the observation button to switch to a full-screen, passive display: a slow-moving starfield/warp view framed by the current ship's viewport, with an activity slider to control how much visual motion happens. It's designed to be left running on a second monitor or TV as ambient scenery, or projected in a room. Click the background (or press Escape) to return to the controls.
|
||||
Click WATCH EXPERIENCE to switch to a full-screen, passive display: a slow-moving starfield/warp view framed by the current ship's viewport, with an activity slider to control how much visual motion happens. Each universe draws its own waveform readout down there too — subspace harmonics on a Starflight bridge, a drive vibration scope on an industrial tug, tactical carrier blocks on a military deck. It's designed to be left running on a second monitor or TV as ambient scenery, or projected in a room. Click the background (or press Escape) to return to the controls.
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
@@ -58,9 +86,9 @@ Click the observation button to switch to a full-screen, passive display: a slow
|
||||
| 0 | Set volume to 100% |
|
||||
| W | Trigger the universe's primary event (e.g. warp jump) |
|
||||
| R | Trigger an alert |
|
||||
| F | Toggle the observation viewport frame (in Observation Mode) |
|
||||
| P | Toggle support pillars (in Observation Mode) |
|
||||
| Esc | Exit Observation Mode / stop active alerts |
|
||||
| F | Toggle the viewport frame (in the Watch Experience) |
|
||||
| P | Toggle support pillars (in the Watch Experience) |
|
||||
| Esc | Exit the Watch Experience / stop active alerts |
|
||||
|
||||
---
|
||||
|
||||
@@ -77,12 +105,15 @@ SciFi-XZBT/
|
||||
├── js/
|
||||
│ ├── audio.js # Web Audio synthesis, oscillators, sound engines
|
||||
│ ├── config.js # Vessel presets, universe registry & palettes
|
||||
│ ├── core-animations.js # Settings-panel core animations
|
||||
│ ├── visualizer.js # Audio spectrum & Worp Core canvas rendering
|
||||
│ ├── observation-bezels.js # Procedural SVG viewport bezels
|
||||
│ ├── observation-engine.js # Observation mode canvas and simulation engine
|
||||
│ ├── observation-engine.js # Watch Experience canvas and simulation engine
|
||||
│ ├── midi.js # Control bus & Web MIDI / MIDI-learn controller
|
||||
│ ├── generative-experience.js # Optional local LLM narration & neural speech
|
||||
│ └── app.js # DOM bindings, hotkeys, UI controllers & main loop
|
||||
├── dist/
|
||||
│ └── SciFiAmbientDisplay_v4.html # Standalone, single-file offline build
|
||||
│ └── SciFiAmbientDisplay_V<commit>.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
|
||||
@@ -93,8 +124,11 @@ SciFi-XZBT/
|
||||
- Edit the specific CSS or JS files directly.
|
||||
- Audio/synthesizer logic lives in `js/audio.js`.
|
||||
- Viewscreen bezels and framing are in `js/observation-bezels.js`.
|
||||
- Observation-mode canvases and simulation are in `js/observation-engine.js`.
|
||||
- Watch Experience canvases and simulation are in `js/observation-engine.js`.
|
||||
- Presets and the sound matrix live in `js/config.js`.
|
||||
- FNC lives in `js/audio.js`; MIDI and the control bus in `js/midi.js`.
|
||||
- `js/generative-experience.js` is the only file that touches the network, and only
|
||||
when the operator prepares it. See `agents.md` for the bounds on that exception.
|
||||
|
||||
### Repackaging to a single file
|
||||
|
||||
@@ -102,6 +136,6 @@ SciFi-XZBT/
|
||||
powershell -ExecutionPolicy Bypass -File .\tools\package.ps1
|
||||
```
|
||||
|
||||
This produces `dist\SciFiAmbientDisplay_v4.html`, combining all CSS and JS back into a self-contained, double-clickable offline file.
|
||||
This produces `dist\SciFiAmbientDisplay_V<commit>.html`, combining all CSS and JS back into a self-contained, double-clickable offline file. It requires git, since the commit hash is what labels the build.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -14,12 +14,13 @@ package manager, no server, and no test suite.**
|
||||
|
||||
## Architecture
|
||||
|
||||
`index.html` loads one stylesheet and six scripts **in a fixed order**:
|
||||
`index.html` loads one stylesheet and eight scripts **in a fixed order**:
|
||||
|
||||
```
|
||||
css/style.css
|
||||
js/audio.js -> js/config.js -> js/visualizer.js
|
||||
js/observation-bezels.js -> js/observation-engine.js -> js/app.js
|
||||
js/audio.js -> js/config.js -> js/core-animations.js -> js/visualizer.js
|
||||
js/observation-bezels.js -> js/observation-engine.js
|
||||
js/midi.js -> js/generative-experience.js -> js/app.js
|
||||
```
|
||||
|
||||
Order matters: every file defines plain classes/objects and publishes them on
|
||||
@@ -28,16 +29,19 @@ tags without checking the dependencies.
|
||||
|
||||
| File | Owns | ~Lines |
|
||||
| --- | --- | --- |
|
||||
| `index.html` | DOM skeleton, script/link tags | 370 |
|
||||
| `css/style.css` | LCARD themes, CRT effects, palettes | 2,300 |
|
||||
| `js/audio.js` | `AudioManager` + all synth classes (hull, warp, life support, telemetry, alert, Whataverse, expanded sci-fi) | 2,250 |
|
||||
| `js/config.js` | `StarshipPresets`, `UniverseRegistry` — data only, no behavior | 1,040 |
|
||||
| `js/visualizer.js` | `StarshipVisualizer` — spectrum + worp core canvas | 640 |
|
||||
| `index.html` | DOM skeleton, script/link tags | 460 |
|
||||
| `css/style.css` | LCARD themes, CRT effects, palettes | 2,430 |
|
||||
| `js/audio.js` | `AudioManager`, `FuturisticNoiceCancellation` + all synth classes (hull, warp, life support, telemetry, alert, Whataverse, expanded sci-fi) | 6,890 |
|
||||
| `js/config.js` | `StarshipPresets`, `UniverseRegistry` — data only, no behavior | 1,170 |
|
||||
| `js/core-animations.js` | `CoreAnimations` — settings-panel core animations | 220 |
|
||||
| `js/visualizer.js` | `StarshipVisualizer` — spectrum + worp core canvas | 670 |
|
||||
| `js/observation-bezels.js` | `ObservationBezels` — procedural SVG viewport frames per era | 270 |
|
||||
| `js/observation-engine.js` | `ObservationEngine` — celestial simulation and canvas rendering | 2,780 |
|
||||
| `js/app.js` | DOM bindings, hotkeys, UI controllers, main loop | 3,960 |
|
||||
| `js/observation-engine.js` | `ObservationEngine` — celestial simulation, canvas rendering, per-universe waveform | 3,590 |
|
||||
| `js/midi.js` | `XZBTControlBus` (semantic control registry) + `XZBTMidiController` (Web MIDI / MIDI learn) | 170 |
|
||||
| `js/generative-experience.js` | `XZBTGenerativeExperience` — optional local LLM narration and neural speech | 1,370 |
|
||||
| `js/app.js` | DOM bindings, hotkeys, UI controllers, main loop | 4,440 |
|
||||
| `tools/package.ps1` | Builds the standalone single-file HTML | — |
|
||||
| `dist/SciFiAmbientDisplay_v4.html` | **Generated output — never edit by hand** | — |
|
||||
| `dist/SciFiAmbientDisplay_V<commit>.html` | **Generated output — never edit by hand** | — |
|
||||
|
||||
## Hard constraints
|
||||
|
||||
@@ -45,6 +49,32 @@ tags without checking the dependencies.
|
||||
audio files. The packaged build must work when double-clicked while offline.
|
||||
- **No ES modules.** No `import` / `export`, no `type="module"`. Scripts are
|
||||
classic globals.
|
||||
|
||||
### The one documented exception
|
||||
|
||||
`js/generative-experience.js` is the **only** file permitted to reach the
|
||||
network, and it is the only file permitted to use `import`. It lazily pulls
|
||||
WebLLM and Kokoro via dynamic `import()` from CDN:
|
||||
|
||||
```js
|
||||
this.webllm = this.webllm || await import('https://esm.run/@mlc-ai/web-llm');
|
||||
this.kokoroModule = this.kokoroModule || await import('https://esm.sh/[email protected]?bundle');
|
||||
```
|
||||
|
||||
The exception is bounded, and every bound is load-bearing:
|
||||
|
||||
- The imports live **inside** `prepareExperience()` / `prepareTTS()`. Nothing is
|
||||
fetched at parse time, so the packaged single-file build still opens and runs
|
||||
fully offline — the Generative Experience panel simply stays on STANDBY.
|
||||
- Both are guarded: `navigator.gpu` is checked before the WebLLM import, and a
|
||||
failed Kokoro load falls back rather than throwing.
|
||||
- No other subsystem may depend on this one. The deterministic engine stays
|
||||
authoritative; the LLM only verbalizes world state that audio.js and app.js
|
||||
already decided.
|
||||
|
||||
Do not widen this exception. New network use anywhere else, a static
|
||||
`<script type="module">`, or a top-level `import` in any file is still a bug.
|
||||
Web MIDI (`js/midi.js`) is a browser API, not a dependency, and is unaffected.
|
||||
- **Publish globals explicitly.** End each file with `window.Thing = Thing;`,
|
||||
matching the existing pattern.
|
||||
- **All audio is synthesized**, not sampled. New sounds are built from
|
||||
@@ -54,6 +84,9 @@ tags without checking the dependencies.
|
||||
|
||||
- New room or ship preset, universe, palette → `js/config.js`
|
||||
- Sound generation, oscillators, envelopes, filters → `js/audio.js`
|
||||
- FNC comfort-filter behavior, source attenuation curves → `FuturisticNoiceCancellation` in `js/audio.js`
|
||||
- MIDI mapping, learn flow, new controllable targets → `js/midi.js` (register targets on the bus in `js/app.js`)
|
||||
- Speech profiles, prompts, LLM/TTS handling → `js/generative-experience.js`
|
||||
- Spectrum bars, warp-core animation → `js/visualizer.js`
|
||||
- Viewscreen frame / bezel shapes per era → `js/observation-bezels.js`
|
||||
- Planets, stars, drift, observation-mode canvas → `js/observation-engine.js`
|
||||
@@ -104,20 +137,37 @@ extraction script, kept for reference only.
|
||||
There are no automated tests. Verification is manual:
|
||||
|
||||
1. Open `index.html` in a browser; check the console is clean.
|
||||
2. Exercise the affected area — play a preset, switch era/universe, open
|
||||
observation mode.
|
||||
3. Re-run `package.ps1` and open `dist/SciFiAmbientDisplay_v4.html` **with the
|
||||
network disabled** to confirm it is genuinely self-contained.
|
||||
2. Exercise the affected area — play a preset, switch era/universe, enter the
|
||||
Watch Experience, toggle FNC.
|
||||
3. Re-run `package.ps1` and open the generated `dist/SciFiAmbientDisplay_V<commit>.html`
|
||||
**with the network disabled** to confirm it is genuinely self-contained. The
|
||||
Generative Experience panel must report an error and everything else must
|
||||
still work — that is the test for the exception above, not a failure.
|
||||
|
||||
Browser autoplay policy blocks audio until a user gesture; audio starting only
|
||||
after a click is expected behavior, not a bug.
|
||||
|
||||
The Generative Experience needs a WebGPU-capable browser (Chrome/Edge) **and**
|
||||
network access on first preparation. Absence of either is expected to degrade
|
||||
quietly, never to break the console.
|
||||
|
||||
## Conventions and cautions
|
||||
|
||||
- 2-space indentation; single quotes in JS; existing brace and spacing style.
|
||||
- `js/app.js` and `js/observation-engine.js` are large. Make **surgical, scoped
|
||||
edits** — do not rewrite, reorder, or reformat whole files.
|
||||
- `js/app.js`, `js/audio.js` and `js/observation-engine.js` are large. Make
|
||||
**surgical, scoped edits** — do not rewrite, reorder, or reformat whole files.
|
||||
- Do not reformat or lint files wholesale; diffs should stay minimal and
|
||||
reviewable.
|
||||
- Never edit anything in `dist/` — regenerate it instead.
|
||||
- Preserve the offline, single-file-deliverable property in every change.
|
||||
|
||||
## Terminology
|
||||
|
||||
`Theme_Terminology_IP_Changes.md` is the authoritative rename table and applies
|
||||
to **identifiers as well as display strings**. The universe id is `whataverse`,
|
||||
the theme class is `theme-whataverse-tardix`, preset ids are `tardix-*`, and the
|
||||
telemetry era is `'whataverse'`. Do not reintroduce the original franchise
|
||||
spellings anywhere, including in comments.
|
||||
|
||||
`TelemetrySynth` still accepts `'who'` and `'what'` as era values purely for
|
||||
backward compatibility with older presets; do not write new presets against them.
|
||||
|
||||
-182
@@ -1,182 +0,0 @@
|
||||
# Audio Implementation Gaps & Procedural Synthesis Backlog
|
||||
|
||||
This document consolidates all procedural audio gaps, missing discrete sound effects, and atmospheric enhancements identified across the 10 universe themes in [sound_reference.md](sound_reference.md).
|
||||
|
||||
Its purpose is to serve as an actionable, prioritized implementation roadmap for future procedural audio synthesis passes in js/audio.js and js/config.js, organized strictly by **implementation difficulty** to enable structured sprints—from immediate quick wins to complex multi-oscillator synthesis engines.
|
||||
|
||||
---
|
||||
|
||||
## 1. Master Gaps Table (Sorted by Difficulty)
|
||||
|
||||
The table below catalogs all **54 discrete sound gaps** identified across the 10 sci-fi universes, sorted in ascending order of technical implementation difficulty:
|
||||
1. **Low** (16 items) — Immediate quick wins: discrete envelope triggers, simple frequency glides, noise bursts, filtered transients, and klaxons.
|
||||
2. **Low–Medium** (2 items) — Simple compound one-shots (e.g., impact + electrical buzz, friction creak + mechanical latch).
|
||||
3. **Medium** (25 items) — Multi-oscillator patches, resonant filter sweeps, LFO-modulated continuous beds, and procedural loop parameters.
|
||||
4. **Medium–High** (2 items) — Multi-voice textured noise beds and bespoke telemetry state engines.
|
||||
5. **High** (9 items) — Multi-stage complex audio state machines, cascaded oscillator chains, mechanical rhythm generators, and full universe telemetry router overhauls.
|
||||
|
||||
| # | Element / Sound | Theme | Ship(s) / Source Work | Description & Acoustic Rationale | Implementation Method & Runtime Trigger Conditions | Priority | Difficulty | Status |
|
||||
|---|---|---|---|---|---|---|---|:---:|
|
||||
| 1 | **Comm badge chirp** | Star Trek | TNG, VOY (Galaxy, Intrepid) | The most recognizable discrete sound in Star Trek; short, bright, iconic two-tone confirmation chirp. | `ExpandedSciFiAudioSynth.synthesizeCommBadge()` & `TelemetrySynth.synthesizeCommBadge()`.<br>• **Soundboard:** Starfleet keypad (`btn-comm-badge`).<br>• **Ambient Loop:** Scheduled randomly in background Starfleet bridge telemetry during `tng`/`voyager` playback (8–10% probability). | High | **Low** | **Implemented** |
|
||||
| 2 | **Door swish** | Star Trek | TOS, TNG, VOY, ENT | Pneumatic air-release door cycle. Four distinct era variants derivable from one base noise generator with altered envelope slopes. | `ExpandedSciFiAudioSynth.synthesizeDoorSwish(era)`.<br>• **Soundboard:** Starfleet keypad (`btn-door-swish`).<br>• **Dynamic Tuning:** Automatically adapts bandpass center frequency, filter Q, and decay duration to the active ship preset's era (`tos`, `tng`, `voyager`, `nx`). | High | **Low** | **Implemented** |
|
||||
| 3 | **Bosun's whistle** | Star Trek | TOS (USS Enterprise) | Traditional naval two-tone pipe whistle heralding command announcements on 23rd-century Starfleet vessels. | `ExpandedSciFiAudioSynth.synthesizeBosunWhistle()`.<br>• **Soundboard:** Starfleet keypad (`btn-bosun-whistle`). Swept sine glide (1800 to 2400 Hz) with 6.5 Hz tremolo. | Medium | **Low** | **Implemented** |
|
||||
| 4 | **Medical monitor (Sickbay ECG)** | Star Trek | TNG, VOY | Calm, periodic rhythmic vital signs pulse indicating stable bio-function in starship sickbays. | `ExpandedSciFiAudioSynth.synthesizeMedicalMonitor()`.<br>• **Soundboard:** Starfleet keypad (`btn-medical-monitor`).<br>• **Ambient Loop:** Activated automatically as a periodic 1.15s pulse (`startMedicalMonitor`) whenever medical/sickbay presets are engaged. | Medium | **Low** | **Implemented** |
|
||||
| 5 | **Lever-throw clunk** | Whoniverse | All TARDIS Console Rooms | Tactile, heavy mechanical lever throw preceding dematerialization and time rotor engagement. | `WhoniverseAudioSynth.synthesizeLeverClunk()`.<br>• **Soundboard:** TARDIS controls (`btn-lever-clunk`). Hard square wave transient click (80 Hz) paired with a dull highpass pink noise thump. | High | **Low** | **Implemented** |
|
||||
| 6 | **Misty-atmosphere life-support tuning** | Bioships | Wraith Hive Ship | Low-lying, humid corridor moisture and condensation clinging to organic interior bulkheads. | Preset tuning in `js/config.js` (`StarshipPresets['bio-wraith'].lifeSupport`).<br>• **Ambient Loop:** Continuously active in `LifeSupportSynth` during `bio-wraith` preset playback (lowpass filter cutoff at 900 Hz, slow 0.06 Hz airflow modulation, depth 0.22). | Medium | **Low** | **Implemented** |
|
||||
| 7 | **Sevastopol "spark" transient** | Space Stations | Sevastopol Station (*Alien: Isolation*) | Intermittent, high-voltage electrical discharge arcs jumping across damaged station conduits. | `ExpandedSciFiAudioSynth.synthesizeSevastopolSpark()`.<br>• **Soundboard:** Station ops (`btn-sevas-spark`).<br>• **Ambient Loop:** Automatic background discharge routine (`startStationSparks`) firing randomized highpass spark clusters every 4–11 seconds while `sta-sevastopol` preset is active. | High | **Low** | **Implemented** |
|
||||
| 8 | **Commlock calling tone** | Space Stations | Moonbase Alpha (*Space: 1999*) | The iconic dual-frequency beep sequence emitted when paging staff via handheld Alpha commlocks. | `ExpandedSciFiAudioSynth.synthesizeCommlockTone()`.<br>• **Soundboard:** Station ops (`btn-commlock`). Dual square-wave pulse sequence (1100 Hz for 60 ms, 1500 Hz for 75 ms). | Medium | **Low** | **Implemented** |
|
||||
| 9 | **Medical vital telemetry (ECG)** | Space Stations | Gateway Station (*Aliens*) | Sterile clinical heart rate monitor chirp in quarantine medical suites. | `ExpandedSciFiAudioSynth.synthesizeStationMedicalPing()`.<br>• **Soundboard:** Station ops (`btn-station-ecg`).<br>• **Ambient Loop:** Automated 1.15s periodic vital telemetry loop (`startMedicalMonitor`) engaged while `sta-gateway` quarantine medical preset is active. | Low | **Low** | **Implemented** |
|
||||
| 10 | **Solar radiation / heat shield roar** | Deep Space | Icarus II (*Sunshine*) | Terrifying low-frequency sonic roar of intense solar wind deflectors grazing the solar corona. | `ExpandedSciFiAudioSynth.synthesizeSolarRoar()`.<br>• **Soundboard:** Void console (`btn-solar-roar`).<br>• **Scripted Event:** Triggered via Deep Space universe event button `btn-solar-swell` ("SOLAR SHIELD SWELL"). Lowpass brown noise body (160 Hz) with 3.6s swell and highpass crackle. | Medium | **Low** | **Implemented** |
|
||||
| 11 | **Cryogenic pod depressurization sigh** | Deep Space | Avalon, The Ark One | Pneumatic gas pressure relief followed by motorized seal depressurization when waking colonists. | `ExpandedSciFiAudioSynth.synthesizeCryoDepressurize()`.<br>• **Soundboard:** Void console (`btn-cryo-sigh`). Swept lowpass noise depressurization (1800 to 280 Hz) terminating in a quiet 205 Hz motorized seal hum. | Low | **Low** | **Implemented** |
|
||||
| 12 | **Mid-century Orion nuclear pulse thump** | Deep Space | USS Ascension (*Ascension*) | Heavy, rhythmic industrial thud of giant mechanical flywheel clutches and nuclear pusher-plate dampeners. | `ExpandedSciFiAudioSynth.synthesizeOrionPulseThump()`.<br>• **Soundboard:** Void console (`btn-orion-thump`). Heavy 45 Hz lowpass square impulse with 36 Hz sub-bass cabinet resonance. | Low | **Low** | **Implemented** |
|
||||
| 13 | **Dedicated Tea Dispenser Gurgle** | Comedy & Adventure | Heart of Gold (*Hitchhiker's Guide*) | Comedic liquid bubble, bubbling gurgle, and steam hiss of the Sirius Cybernetics Nutri-Matic machine. | `ExpandedSciFiAudioSynth.synthesizeTeaDispenser()`.<br>• **Soundboard:** Cheerful keypad (`btn-tea-machine`). 6 bandpass bubbling noise hops (480–960 Hz) terminating in a 2400 Hz steam hiss. | High | **Low** | **Implemented** |
|
||||
| 14 | **British Electric Kettle Steam Whistle** | Comedy & Adventure | HMS Camden Lock (*Hyperdrive*) | Whistling domestic electric tea kettle echoing down gray naval corridors during red alerts. | `ExpandedSciFiAudioSynth.synthesizeKettleWhistle()`.<br>• **Soundboard:** Cheerful keypad (`btn-kettle-whistle`). Swept sine oscillator (1820 to 2380 Hz) with 5.2 Hz tremolo. | Low | **Low** | **Implemented** |
|
||||
| 15 | **Ludicrous Speed "Plaid" Alarm** | Comedy & Adventure | Spaceball One (*Spaceballs*) | Hyperactive, comedic electronic alarm horn sounding when exceeding Ludicrous Speed. | `ExpandedSciFiAudioSynth.synthesizePlaidAlarm()`.<br>• **Soundboard:** Cheerful keypad (`btn-plaid-alarm`). Rapid alternating square wave siren (440 Hz / 880 Hz, 120 ms bursts across 6 steps). | Low | **Low** | **Implemented** |
|
||||
| 16 | **Dedicated Cassette Transport Clunk & Whirr** | Outlaw & Frontier | The Milano, Bebop | Spring-loaded mechanical tape deck button click followed by 12V capstan motor whirr. | `ExpandedSciFiAudioSynth.synthesizeCassetteTransport()`.<br>• **Soundboard:** Outlaw cockpit (`btn-tape-clunk`). Dual square transient click (120 Hz / 360 Hz) followed by 2200 Hz capstan motor tone with 8.2 Hz flutter and pink noise tape bed. | High | **Low** | **Implemented** |
|
||||
| 17 | **Grappler Arm Servo Motor Whine** | Outlaw & Frontier | Outlaw Star, Bebop | Articulated hydraulic/electric grapple arms extending and clamping onto target vessels. | `ExpandedSciFiAudioSynth.synthesizeGrapplerServo()`.<br>• **Soundboard:** Outlaw cockpit (`btn-grappler-servo`). Swept triangle wave (220 to 680 Hz) through resonant bandpass filter (Q 3.2). | High | **Low** | **Implemented** |
|
||||
| 18 | **True RF Radio Static Burst** | Outlaw & Frontier | The Betty, The Marauder | Rugged CB-radio squelch break and atmospheric RF hash between short-range ship comms. | `ExpandedSciFiAudioSynth.synthesizeRadioStatic()`.<br>• **Soundboard:** Outlaw cockpit (`btn-radio-static`). Bandpass-filtered pink/white noise burst (1400 Hz, Q 1.1, hard-knee 180 ms gate). | High | **Low** | **Implemented** |
|
||||
| 19 | **TARDIS door open/close** | Whoniverse | All TARDIS Console Rooms | Friction creak of heavy wooden police box exterior doors followed by an iron mortise lock click. | `WhoniverseAudioSynth.synthesizeTardisDoor()`.<br>• **Soundboard:** TARDIS controls (`btn-tardis-door`). Bandpass friction noise (wood creak, 260 to 540 Hz) layered over dual metallic mortise latch clicks (1100 Hz / 750 Hz). | High | **Low–Medium** | **Implemented** |
|
||||
| 20 | **Magnetic boot ("mag-boot") latch** | Space Stations | Ceres, Tycho (*The Expanse*) | Heavy physical boot heel impact on iron decking immediately followed by an electromagnetic clamping buzz. | `ExpandedSciFiAudioSynth.synthesizeMagBootLatch()`.<br>• **Soundboard:** Station ops (`btn-mag-boot`). 80 Hz impact click (30 ms) followed instantly by a 120 Hz square-wave clamping buzz (160 ms decay). | High | **Low–Medium** | **Implemented** |
|
||||
| 21 | **Cardassian door** | Star Trek | DS9 (Terok Nor) | Oppressive, heavy Cardassian pneumatic bulkheads grinding open along metal gears. | `ExpandedSciFiAudioSynth.synthesizeCardassianDoor()`.<br>• **Soundboard:** Starfleet keypad (`btn-cardassian-door`). Resonant brown-noise bandpass sweep (120→650 Hz, Q 3.5) layered with a scraping 110→70 Hz saw undertone. | High | **Medium** | **Implemented** |
|
||||
| 22 | **Turbolift** | Star Trek | TNG, VOY | High-speed magnetic turbolift car passing through deck shafts with a rising whoosh and decelerating settle. | `ExpandedSciFiAudioSynth.synthesizeTurboliftWhoosh()`.<br>• **Soundboard:** Starfleet keypad (`btn-turbolift`). White-noise bandpass sweep (180→450→180 Hz, Q 2.2) with −0.45→+0.45 stereo pan drift. | Medium | **Medium** | **Implemented** |
|
||||
| 23 | **Replicator** | Star Trek | TNG, VOY | Molecular dematerialization / materialization shimmer when dispensing food or tools. | `ExpandedSciFiAudioSynth.synthesizeReplicatorShimmer()`.<br>• **Soundboard:** Starfleet keypad (`btn-replicator`). Dual parallel bands (3.2 kHz / 6.4 kHz) of white noise with 30 Hz sine AM and 2 s ramp decay. | Low | **Medium** | **Implemented** |
|
||||
| 24 | **Scanner/monitor activation** | Whoniverse | All TARDIS Console Rooms | Distinctive cathode-ray / crystal scanner screen powering on above the central console. | `WhoniverseAudioSynth.synthesizeScannerActivate()`.<br>• **Soundboard:** TARDIS controls (`btn-scanner-activate`). Sine sweep (200→1600 Hz / 120 ms) terminating in a 10.6 kHz CRT flyback partial (15.6 kHz spec compromised: near-Nyquist/ear-fatiguing). | Medium | **Medium** | **Implemented** |
|
||||
| 25 | **Console "type" input telemetry** | Whoniverse | All TARDIS Console Rooms | Rhythmic, mechanical clatter of physical toggle switches, spring-loaded buttons, and tumbler relays. | `WhoniverseAudioSynth.synthesizeTypeClatter()` + `startConsoleTypeTelemetry()`.<br>• **Soundboard:** TARDIS controls (`btn-type-clatter`).<br>• **Ambient Loop:** Random-window clatter clusters (2.8–6.5 s) on every `tardis-*` console room. Delivered as a standalone routine; `TelemetrySynth` `era: 'who'` routing remains Phase 3. | High | **Medium** | **Implemented** |
|
||||
| 26 | **"Singing" resonance element** | Bioships | Vorlon Cruiser (*Babylon 5*) | Ethereal, telepathic crystal harmonics echoing through living Vorlon organic hulls. | `ExpandedSciFiAudioSynth.synthesizeVorlonSingingSwell()` + `startVorlonSong()`.<br>• **Soundboard:** Bioship keypad (`btn-vorlon-song`).<br>• **Ambient Loop:** 8.5 s swell phrases (11–18 s cadence) while `bio-vorlon` is engaged. Dual detuned sines (528/532 Hz) through narrow bandpass (Q 22) with 0.2 Hz tremolo. | High | **Medium** | **Implemented** |
|
||||
| 27 | **Neural-bond swell** | Bioships | Moya, Talyn, Wraith Hive | Symbiotic neural link between pilot and bioship swelling during heightened emotional or flight states. | `ExpandedSciFiAudioSynth.synthesizeNeuralBondSwell()`.<br>• **Soundboard:** Bioship keypad (`btn-neural-bond`).<br>• **Scripted Event:** Bioship universe event `btn-neural-pulse` ("NEURAL PULSE") now fires this swell. Deep 55 Hz triangle sweeping through a vowel-formant bandpass (350→850 Hz, Q 4.5) over 2.5 s. | Medium | **Medium** | **Implemented** |
|
||||
| 28 | **Hull self-repair / regeneration texture** | Bioships | Wraith Hive Ship | Microscopic organic tissue knit and chitinous shell regrowth during ship recovery phases. | `ExpandedSciFiAudioSynth.synthesizeHullRegenBurst()` + `startHullRegen()`.<br>• **Soundboard:** Bioship keypad (`btn-hull-regen`).<br>• **Ambient Loop:** Regeneration bursts (6–13 s cadence) while `bio-wraith` is engaged. Granular square-LFO AM (9–17 Hz) over bandpass pink noise plus wet chitin knit pops. | Low | **Medium** | **Implemented** |
|
||||
| 29 | **Belter "jury-rigged" telemetry era** | Space Stations | Ceres, Tycho (*The Expanse*) | Rattle of loose relays, worn copper contactors, and erratic voltage drops across secondhand station hardware. | `ExpandedSciFiAudioSynth.synthesizeBelterTelemetryJitter()` + `startBelterTelemetry()`.<br>• **Soundboard:** Station ops (`btn-belter-telemetry`, full-width).<br>• **Ambient Loop:** Jittered clusters (1.8–4.5 s cadence) while `sta-ceres`/`sta-tycho` are engaged. Delivered as a standalone routine; `TelemetrySynth` `era: 'belter'` routing remains Phase 3. | High | **Medium** | **Implemented** |
|
||||
| 30 | **True Hull Strain Moan** | Deep Space | Event Horizon, Ark One, Icarus II | Massive structural groan and creak of stressed titanium and composite bulkheads flexing under gravitational shear. | `ExpandedSciFiAudioSynth.synthesizeHullStrainMoan()` + `startHullStrainMoans()`.<br>• **Soundboard:** Void console (`btn-hull-strain`).<br>• **Ambient Loop:** Sporadic moans (16–40 s cadence) on `deep-event-horizon`/`deep-icarus-ii`. Brown-noise bandpass sweep (48→210 Hz, Q 10) with asymmetric attack/decay. | High | **Medium** | **Implemented** |
|
||||
| 31 | **Icarus I distress beacon** | Deep Space | Icarus II (*Sunshine*) | Eerie, hypnotic modal electronic arpeggio echoing across the void from the ghost ship. | `ExpandedSciFiAudioSynth.synthesizeIcarusBeacon()` + `startIcarusBeacon()`.<br>• **Soundboard:** Void console (`btn-icarus-beacon`).<br>• **Ambient Loop:** Modal arpeggio (22–40 s cadence) while `deep-icarus-ii` is engaged. 4-note A-minor modal sine sequence through a 0.45 s feedback delay with 2.2 kHz dampening. | High | **Medium** | **Implemented** |
|
||||
| 32 | **Extradimensional / psychic static whisper** | Deep Space | Event Horizon, Nightflyer | Menacing psychoacoustic hallucination burst simulating whispers bleeding through dimensional rifts. | `ExpandedSciFiAudioSynth.synthesizeVoidWhisper()` + `startVoidWhispers()`.<br>• **Soundboard:** Void console (`btn-void-whisper`).<br>• **Ambient Loop:** Whisper bursts (15–32 s cadence) on `deep-event-horizon`/`deep-nightflyer`. Three wobbling vowel-formants (700/1250/2200 Hz) with randomized syllable micro-envelopes. | Medium | **Medium** | **Implemented** |
|
||||
| 33 | **Beryllium Sphere Resonant Thrum** | Comedy & Adventure | NSEA Protector (*Galaxy Quest*) | Deep, glassy, crystalline reactor hum vibrating with pure alien harmonic energy. | `ExpandedSciFiAudioSynth.synthesizeBerylliumThrumSwell()` + `startBerylliumThrum()`.<br>• **Soundboard:** Cheerful keypad (`btn-beryllium-thrum`).<br>• **Ambient Loop:** 10 s thrum swell phrases (9–16 s cadence) while `com-protector` is engaged. Pure sines (65 + 65.3 Hz beating + 195 Hz third) ringing through a 5.1 ms comb reflection. | Medium | **Medium** | **Implemented** |
|
||||
| 34 | **Omega-13 Temporal Capacitor Whine** | Comedy & Adventure | NSEA Protector (*Galaxy Quest*) | Tremendous temporal capacitor power buildup rising into a deafening dimensional snap. | `ExpandedSciFiAudioSynth.synthesizeOmega13Whine()`.<br>• **Soundboard:** Cheerful keypad (`btn-omega-13`). 40 Hz sub-bass exponentially ramping to 3200 Hz over 3 s, culminating in a wide highpass white-noise discharge pop. | High | **Medium** | **Implemented** |
|
||||
| 35 | **Repulsorlift Engine Drone** | Outlaw & Frontier | The Marauder (*Star Wars*) | Quintessential atmospheric anti-gravity vehicle wash and repulsor grid drone. | `ExpandedSciFiAudioSynth.synthesizeRepulsorliftDrone()` + `startRepulsorliftDrone()`.<br>• **Soundboard:** Outlaw cockpit (`btn-repulsor-drone`).<br>• **Ambient Loop:** 10 s wash phrases (13 s cadence) while `out-marauder` is engaged. Dual detuned sines (68/72 Hz) through a tanh waveshaper (2x oversampled) and 280 Hz lowpass. | Medium | **Medium** | **Implemented** |
|
||||
| 36 | **Converted Marine Trawler Engine Chug** | Outlaw & Frontier | Bebop (*Cowboy Bebop*) | Rhythmic, heavy diesel-like marine piston strokes echoing through the Bebop's converted hull. | `ExpandedSciFiAudioSynth.synthesizeBebopChugStroke()` + `startBebopChug()`.<br>• **Soundboard:** Outlaw cockpit (`btn-bebop-chug`).<br>• **Ambient Loop:** 2–4 stroke clusters (2.2–4.2 s cadence) while `out-bebop` is engaged. Square pulses (45–70 Hz) through an 80 Hz resonant lowpass with piston wheeze. | Medium | **Medium** | **Implemented** |
|
||||
| 37 | **Centrifugal Habitat Carousel Motor Groan** | Military Space | EAS Agamemnon (*Babylon 5*) | Low-frequency rotational strain and mechanical bearing hum of the 1 rpm rotating crew carousel. | `ExpandedSciFiAudioSynth.synthesizeCarouselGroanCycle()` + `startCarouselGroan()`.<br>• **Soundboard:** Military keypad (`btn-carousel-groan`).<br>• **Ambient Loop:** One 58 s rotation pass every 60 s while `mil-agamemnon` is engaged. 16 Hz rotational beating pair + 48/96 Hz bearing harmonics, strain-swell LFO at exactly 1/60 Hz. | Medium | **Medium** | **Implemented** |
|
||||
| 38 | **Slipstream Drive Transition Surge** | Military Space | Andromeda Ascendant | Ascending harmonic pitch sweep followed by crystalline string-like resonance upon entering slipstream. | `ExpandedSciFiAudioSynth.synthesizeSlipstreamSurge()`.<br>• **Soundboard:** Military keypad (`btn-slipstream`). Dual staggered saw surges (200→2400 Hz) through bandpass filters with Q escalation (2→10) driving a 120 ms comb shimmer tail. | Medium | **Medium** | **Implemented** |
|
||||
| 39 | **Viper Pilot Oxygen Regulator Demand Valve** | Military Space | Colonial Viper Mk II (*BSG*) | Rhythmic breathing hiss and mechanical click of cockpit life-support demand regulator during high-G combat. | `ExpandedSciFiAudioSynth.synthesizeOxygenRegulatorCycle()` + `startOxygenRegulator()`.<br>• **Soundboard:** Military keypad (`btn-oxygen-reg`).<br>• **Ambient Loop:** Breath cycles (3.4–5.2 s cadence) while `mil-viper` is engaged. 15 ms diaphragm click prefixed to highpass band (1.2–4.5 kHz) white-noise breath. | Medium | **Medium** | **Implemented** |
|
||||
| 40 | **Condensation Pipe Drip & Expansion Ticks** | Industrial Space | USCSS Nostromo, Serenity | Water droplets hitting grated industrial deck plates and irregular metallic pings from cooling steam pipes. | `ExpandedSciFiAudioSynth.synthesizePipeDrip()` + `startPipeDrips()`.<br>• **Soundboard:** Industrial keypad (`btn-pipe-drips`).<br>• **Ambient Loop:** Randomized drips (0.5–3.5 s cadence) while `ind-nostromo` is engaged. High-Q sine pings (1.4–2.6 kHz) with randomized 10–25 ms exponential decays and amplitudes. | Medium | **Medium** | **Implemented** |
|
||||
| 41 | **High-Pressure Continuous Steam / Boiler Venting** | Industrial Space | Serenity, Starbug | Sustained high-pressure thermal exhaust venting from leaky boiler seals and cooling radiators. | `ExpandedSciFiAudioSynth.synthesizeSteamVentSwell()` + `startSteamVent()`.<br>• **Soundboard:** Industrial keypad (`btn-steam-vent`).<br>• **Ambient Loop:** 9 s vent passes (6–10 s cadence) on `ind-serenity`/`ind-starbug`. Looping white noise through a 1.5 kHz bandpass (Q 0.7) with 6–9 Hz boiler micro-flutter. | Medium | **Medium** | **Implemented** |
|
||||
| 42 | **Crash Couch Hydraulic Gimbal / G-Load Strain** | Industrial Space | Rocinante (*The Expanse*) | High-torque hydraulic servos straining and whining as gimbaled couches pivot under 6G burn maneuvers. | `ExpandedSciFiAudioSynth.synthesizeCrashCouchGimbal()`.<br>• **Soundboard:** Industrial keypad (`btn-crash-couch`). Dual-layer FM triangle (140 Hz carrier / 35 Hz modulator, then 152/38 Hz) with lowpass damping tracking the strain swell. | Medium | **Medium** | **Implemented** |
|
||||
| 43 | **HAL 9000 Breathing / Vocal Carrier Ambient Loop** | Retro Future | Discovery One (*2001*) | Chillingly calm, measured human respiration echoing inside Dave Bowman's helmet and HAL's logic core. | `ExpandedSciFiAudioSynth.synthesizeHalBreathCycle()` + `startHalBreathing()`.<br>• **Soundboard:** Retro keypad (`btn-hal-breath`).<br>• **Ambient Loop:** Continuous 3.5 s in/out breaths (1.3 s inhale / 2.2 s exhale) while `ret-discovery-one` is engaged. Looping pink noise bandpass (700 Hz center, Q 1.2). | Medium | **Medium** | **Implemented** |
|
||||
| 44 | **Death Blossom Energy Surge Capacitor Ramp** | Retro Future | Gunstar (*The Last Starfighter*) | Rapid, escalating multi-oscillator power buildup prior to deploying the 360-degree weapons barrage. | `ExpandedSciFiAudioSynth.synthesizeDeathBlossomSurge()`.<br>• **Soundboard:** Retro keypad (`btn-death-blossom`). Three cascaded saws (0/150/300 ms offsets) ramping 200→8000 Hz over 2.5 s through rising highpass, cut off abruptly at full bloom. | Medium | **Medium** | **Implemented** |
|
||||
| 45 | **Victorian Steam Piston Chug & Iron Camshaft Clatter** | Retro Future | USS Cygnus (*The Black Hole*) | Massive reciprocating iron steam pistons and heavy camshaft linkages turning the Cygnus's cathedral engines. | `ExpandedSciFiAudioSynth.synthesizeCygnusPistonChug()` + `startCygnusChug()`.<br>• **Soundboard:** Retro keypad (`btn-cygnus-chug`).<br>• **Ambient Loop:** 2–3 stroke clusters (1.8–3.2 s cadence) while `ret-cygnus` is engaged. 40 Hz triangle thumps with lowpass-filtered steam puffs and a 0.35 s metallic slapback. | Medium | **Medium** | **Implemented** |
|
||||
| 46 | **Zocalo commerce-plaza chatter** | Space Stations | Babylon 5 | Bustling multi-species concourse background bed with indistinct alien murmurs and footsteps. | Multi-voice filtered pink noise bed shaped by 4 randomized parallel formant filters (300–3000 Hz) simulating crowd presence. | Low | **Medium–High** | Backlog |
|
||||
| 47 | **Bespoke bioship telemetry era** | Bioships | Moya, Lexx, Species 8472, Wraith | Complete replacement of inorganic Starfleet chirps with organic clicks, wet pulses, and membrane flutters. | New telemetry engine era (`bioship`): procedural scheduling of bio-acoustic primitives (spore pops, chitin snaps, vascular beats). | High | **Medium–High** | Backlog |
|
||||
| 48 | **Transporter dematerialization / rematerialization** | Star Trek | All Eras | Iconic Starfleet molecular transport cycle (~3.0 seconds duration, shimmer-dense). | Complex multi-oscillator additive array (5 detuned sines) with interlocking amplitude modulation and high-frequency noise shimmer. | Medium | **High** | Backlog |
|
||||
| 49 | **Dedicated Point-Defense Flak Barrage Burst** | Military Space | Battlestar Galactica | Rapid-fire concussive kinetic detonations ("thud-thud-crack") echoing through the armored hull. | Clustered burst of 4–8 staggered noise transients (10–30 ms offsets) with sub-bass punch (40 Hz) and high-frequency shell crack. | High | **High** | Backlog |
|
||||
| 50 | **Heavy Weapons Arming Breech Clank** | Military Space | USS Sulaco, EAS Agamemnon | Pneumatic solenoid slam and heavy steel breech lock when chambering mass-driver ordnance or railgun slugs. | Multi-stage metallic synthesis: pre-charge pneumatic hiss (80 ms), primary breech slam (60 Hz square impact), and high-Q steel ringoff. | High | **High** | Backlog |
|
||||
| 51 | **Dedicated Mechanical Dot-Matrix / Teletype Chatter** | Industrial Space | USCSS Nostromo | Unbroken rhythmic mechanical typing and carriage chatter of the MU/TH/UR 6000 mainframe output terminal. | Rapid pseudo-random sequence of high-Q 4000 Hz impact noise spikes combined with periodic 12 Hz mechanical stepper-motor step clicks. | High | **High** | Backlog |
|
||||
| 52 | **Heavy Hydraulic Docking Groan & Mechanical Clang** | Industrial Space | USCSS Nostromo, Red Dwarf | Massive ore umbilical latching: low metal friction groan followed by sledgehammer clamp impact and resonant hull ring. | Compound engine: dual swept resonant bandpass filters over brown noise (30–60 Hz) followed by a high-energy metallic anvil transient. | High | **High** | Backlog |
|
||||
| 53 | **Dual-Reel Magnetic Tape Spooler & Pinch Roller Clank** | Retro Future | Jupiter 2, Discovery One | Heavy magnetic tape deck engagement: solenoid pinch-roller snap, 15 ips capstan flutter, and fast-forward tape whine. | Multi-layer module: solenoid impulse click (110 Hz), continuous fluttering pink noise (15 Hz flutter LFO), and 3200 Hz capstan motor hum. | High | **High** | Backlog |
|
||||
| 54 | **Continuous Heterodyne / Theremin Navigational Drone** | Retro Future | Jupiter 2 (*Lost in Space*) | Vintage 1960s sci-fi navigational beacon: dual high-frequency sine oscillators beating gently against each other. | Dual precision sine generators (e.g., 1040 Hz and 1048 Hz) creating a continuous, acoustic 8 Hz heterodyne beat wave in room tone. | High | **High** | Backlog |
|
||||
|
||||
---
|
||||
|
||||
## 2. Implementation Phasing & Roadmap
|
||||
|
||||
To structure the upcoming audio engineering passes without destabilizing the core audio loop, development should follow a three-phase approach:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
P1["Phase 1: Quick Wins<br>18 Discrete Elements<br>Difficulty: Low & Low-Medium"] --> P2["Phase 2: Core Enhancements<br>25 Synthesizers & Loops<br>Difficulty: Medium"]
|
||||
P2 --> P3["Phase 3: Major Architectural Additions<br>11 Complex Systems & Telemetry Eras<br>Difficulty: Medium-High & High"]
|
||||
```
|
||||
|
||||
### Phase 1: Quick Wins (Low & Low–Medium Difficulty) — **[COMPLETED]**
|
||||
- **Target Count**: 20 elements (18 Low + 2 Low–Medium) — **100% Implemented**.
|
||||
- **Architectural Scope**: Added procedural synthesis methods into `ExpandedSciFiAudioSynth` and `WhoniverseAudioSynth`, updated `StarshipPresets['bio-wraith']` life-support parameters in `js/config.js`, wired soundboards to a clean 6-button (3×2) layout, and established automated background loops in `js/app.js`.
|
||||
- **Implemented Deliverables & Trigger Mechanics**:
|
||||
1. **Trek Staples**:
|
||||
- **Comm badge chirp** (`ExpandedSciFiAudioSynth.synthesizeCommBadge`, `TelemetrySynth.synthesizeCommBadge`): Starfleet Soundboard (`btn-comm-badge`) & auto-scheduled in ambient Starfleet telemetry (8–10% probability during `tng`/`voyager`).
|
||||
- **Door swish** (`ExpandedSciFiAudioSynth.synthesizeDoorSwish(era)`): Starfleet Soundboard (`btn-door-swish`), automatically adjusting envelope and filter Q across TOS, TNG, VOY, and NX.
|
||||
- **Bosun's whistle** (`ExpandedSciFiAudioSynth.synthesizeBosunWhistle`): Starfleet Soundboard (`btn-bosun-whistle`), naval sine glide with 6.5 Hz tremolo.
|
||||
- **Sickbay ECG ping** (`ExpandedSciFiAudioSynth.synthesizeMedicalMonitor`, `startMedicalMonitor`): Starfleet Soundboard (`btn-medical-monitor`) & automatic 1.15s periodic pulse loop on sickbay/medical presets.
|
||||
2. **Tactile Hardware & Foley**:
|
||||
- **TARDIS lever clunk** (`WhoniverseAudioSynth.synthesizeLeverClunk`): Whoniverse Soundboard (`btn-lever-clunk`), hard 80 Hz square click + highpass pink noise thump.
|
||||
- **TARDIS wooden door** (`WhoniverseAudioSynth.synthesizeTardisDoor`): Whoniverse Soundboard (`btn-tardis-door`), 260–540 Hz friction creak + dual mortise latch click.
|
||||
- **Cassette transport clunk & whirr** (`ExpandedSciFiAudioSynth.synthesizeCassetteTransport`): Outlaw Soundboard (`btn-tape-clunk`), mechanical dual click + 2200 Hz capstan tone with 8.2 Hz flutter.
|
||||
- **Grappler arm servo** (`ExpandedSciFiAudioSynth.synthesizeGrapplerServo`): Outlaw Soundboard (`btn-grappler-servo`), 220–680 Hz swept triangle wave through resonant bandpass.
|
||||
- **Mag-boot latch** (`ExpandedSciFiAudioSynth.synthesizeMagBootLatch`): Space Stations Soundboard (`btn-mag-boot`), 80 Hz heel impact thump + 120 Hz inductive clamp buzz.
|
||||
3. **Atmospheric, Comedic & Disaster Transients**:
|
||||
- **Misty-atmosphere tuning**: `StarshipPresets['bio-wraith']` tuned to 900 Hz lowpass cutoff with 0.06 Hz airflow modulation in `LifeSupportSynth`.
|
||||
- **Sevastopol electrical spark** (`ExpandedSciFiAudioSynth.synthesizeSevastopolSpark`, `startStationSparks`): Space Stations Soundboard (`btn-sevas-spark`) & intermittent background discharges (4–11s intervals) on `sta-sevastopol`.
|
||||
- **Commlock calling tone** (`ExpandedSciFiAudioSynth.synthesizeCommlockTone`): Space Stations Soundboard (`btn-commlock`), dual square pulse paging beeps (1100 Hz / 1500 Hz).
|
||||
- **Station medical vital ECG** (`ExpandedSciFiAudioSynth.synthesizeStationMedicalPing`): Space Stations Soundboard (`btn-station-ecg`) & 1.15s background loop on Gateway Station quarantine medical suite (`sta-gateway`).
|
||||
- **Solar radiation / heat shield roar** (`ExpandedSciFiAudioSynth.synthesizeSolarRoar`): Deep Space Soundboard (`btn-solar-roar`) & Deep Space Event button `btn-solar-swell` ("SOLAR SHIELD SWELL").
|
||||
- **Cryogenic pod depressurization sigh** (`ExpandedSciFiAudioSynth.synthesizeCryoDepressurize`): Deep Space Soundboard (`btn-cryo-sigh`), swept noise sigh + 205 Hz seal hum.
|
||||
- **Mid-century Orion pulse thump** (`ExpandedSciFiAudioSynth.synthesizeOrionPulseThump`): Deep Space Soundboard (`btn-orion-thump`), 45 Hz lowpass square impulse + 36 Hz sub-bass cabinet resonance.
|
||||
- **Nutri-Matic tea dispenser gurgle** (`ExpandedSciFiAudioSynth.synthesizeTeaDispenser`): Comedy Soundboard (`btn-tea-machine`), 6-stage bubbling hops + steam hiss.
|
||||
- **British electric kettle whistle** (`ExpandedSciFiAudioSynth.synthesizeKettleWhistle`): Comedy Soundboard (`btn-kettle-whistle`), rising sine sweep with 5.2 Hz tremolo.
|
||||
- **Ludicrous Speed Plaid Alarm** (`ExpandedSciFiAudioSynth.synthesizePlaidAlarm`): Comedy Soundboard (`btn-plaid-alarm`), alternating two-tone siren (440 Hz / 880 Hz).
|
||||
- **True RF radio static burst** (`ExpandedSciFiAudioSynth.synthesizeRadioStatic`): Outlaw Soundboard (`btn-radio-static`), bandpass-filtered noise burst with hard-knee 180 ms gate.
|
||||
|
||||
### Phase 2: Core Enhancements (Medium Difficulty) — **[COMPLETED]**
|
||||
- **Target Count**: 25 elements — **100% Implemented**.
|
||||
- **Architectural Scope**: 23 new procedural methods in `ExpandedSciFiAudioSynth` and 2 in `WhoniverseAudioSynth`, a keyed ambient-loop registry (`_scheduleLoop`/`_stopLoop`) replacing ad-hoc timer fields, a single `startPresetAmbience(presetId)` funnel in `js/app.js` that stops and re-engages all loops on preset change/engage/full-stop, and soundboard rebuilds across all 10 universe keypads (6–8 buttons each; `span: 2` support used by the Belter telemetry button).
|
||||
- **Implementation Notes**:
|
||||
- Rows #25 (`era: 'who'`) and #29 (`era: 'belter'`) are delivered as standalone routines (`startConsoleTypeTelemetry`, `startBelterTelemetry`) driven by preset ambience — **not** through `TelemetrySynth` era routing, which remains Phase 3 scope.
|
||||
- #24 CRT flyback whine lands at a 10.6 kHz partial (the spec's 15.6 kHz is near-Nyquist on low-rate audio contexts and ear-fatiguing at audible levels).
|
||||
- Continuous rows (#26, #33, #35, #37, #41) are rendered as slow "phrase" one-shots (8–58 s) with in/out envelopes, re-triggered on fixed/random cadence — no permanent node graphs.
|
||||
- Every ambient phrase self-releases (scheduled node stops) and every `start*` kills its own registry key first; `stopAllLoops()` clears the whole registry, so rapid preset flips and FULL STOP cannot stack loops.
|
||||
- One-shot soundboard buttons are interactive one-shots; ambient rows additionally auto-engage with their matching presets (see per-row trigger details in the master table).
|
||||
- **Key Deliverables**:
|
||||
1. **Structural Stress & Engines**: True Hull Strain Moan (#30, `synthesizeHullStrainMoan` + loop on Event Horizon/Icarus II), Repulsorlift engine drone (#35), Bebop marine diesel chug (#36), Centrifugal carousel 1 rpm LFO modulation (#37), Crash-couch gimbal strain (#42), Cygnus steam piston chug (#45).
|
||||
2. **Energy Fields & Weapons**: Vorlon crystal singing resonance (#26), Omega-13 temporal capacitor ramp (#34), Cardassian heavy bulkhead grind (#21), Slipstream transition surge (#38), Death Blossom capacitor sweep (#44), Icarus I distress beacon (#31), Beryllium sphere thrum (#33).
|
||||
3. **Environmental Ambience**: Nostromo condensation pipe drips (#40, randomized drip clock), Continuous steam vent hiss (#41), HAL 9000 respiration envelope loop (#43), hull regeneration texture (#28), psychic static whisper (#32), turbolift whoosh (#22), replicator shimmer (#23), scanner activation (#24), TARDIS console type clatter (#25), neural-bond swell (#27, also rewired to the Bioships NEURAL PULSE event), Belter jury-rig telemetry (#29), Viper O₂ regulator breathing (#39).
|
||||
|
||||
### Phase 3: Major Architectural Additions (Medium–High & High Difficulty)
|
||||
- **Target Count**: 11 elements.
|
||||
- **Architectural Scope**: Expansion of `TelemetrySynth` routing logic and complex multi-node audio sub-systems.
|
||||
- **Key Deliverables**:
|
||||
1. **Bespoke Telemetry Eras**:
|
||||
- `era: 'who'` — Mechanical solenoids, spring switches, lever snaps, relay chitter.
|
||||
- `era: 'bioship'` — Organic membrane pulses, chitin creaks, bioplasmic fluid pops.
|
||||
- `era: 'belter'` — Rattling relays, noisy contact bounce, loose solenoids, unstable clock jitter.
|
||||
2. **Complex Multi-Stage Transients**:
|
||||
- Battlestar flak barrage burst (staggered concussive noise clusters).
|
||||
- Heavy weapons breech locking clank (pneumatics + steel clamp ringoff).
|
||||
- Nostromo MU/TH/UR dot-matrix teletype terminal (continuous pseudo-random 4000 Hz striker bursts).
|
||||
- Vintage dual-reel magnetic tape spooler (solenoid clamp + tape flutter + capstan tone).
|
||||
- Starfleet Transporter dematerialization / rematerialization shimmer array.
|
||||
|
||||
---
|
||||
|
||||
## 3. Telemetry Router Roadmap
|
||||
|
||||
Currently, `TelemetrySynth` in `js/audio.js` dispatches discrete random pings based solely on Starfleet eras (`tos`, `tng`, `ds9`, `voyager`, `nx`). Non-Trek universes are forced to approximate their telemetry using Trek sound sets.
|
||||
|
||||
The table below outlines the implementation specifications for the three new proposed telemetry engines:
|
||||
|
||||
```text
|
||||
TelemetrySynth.prototype.scheduleNext_()
|
||||
│
|
||||
├── era === 'who' ──> synthesizeWhoMechanicalTelemetry_()
|
||||
├── era === 'bioship' ──> synthesizeBioshipOrganicTelemetry_()
|
||||
├── era === 'belter' ──> synthesizeBelterJuryRiggedTelemetry_()
|
||||
└── era in ['tos'...] ──> existing Starfleet chirp routines
|
||||
```
|
||||
|
||||
| New Telemetry Era | Target Universe(s) | Acoustic Primitives | Synthesis Architecture |
|
||||
|---|---|---|---|
|
||||
| `who` | Whoniverse (All TARDIS presets) | Mechanical toggle snaps, tumbler dials, spring levers, rotary selector clunks. | Dual-filtered short noise pulses (20–45 ms), square wave click transients (60–120 Hz), and damped mechanical pings without electronic beeps. |
|
||||
| `bioship` | Bioships (Moya, Lexx, Species 8472, Wraith) | Vascular surges, chitinous joint snaps, bioplasmic fluid pops, membrane flutters. | Asymmetric shaped sine thumps (30–90 Hz), bandpass-filtered wet fluid impulses (500–1400 Hz), and micro-envelope noise ticks. |
|
||||
| `belter` | Space Stations, Industrial Space (Ceres, Tycho, Rocinante) | Worn electrical contactors, loose solenoid chatter, relay contact bounce, drifting clock pings. | Jittered square waves with randomized micro-dropouts, harsh metallic click transients (400–1800 Hz), and noisy relay chatter bursts. |
|
||||
|
||||
---
|
||||
|
||||
## 4. Cross-Reference Index by Universe Theme
|
||||
|
||||
| Universe Key | Universe Name | Gaps Cataloged | Primary Focus Areas |
|
||||
|---|---|:---:|---|
|
||||
| `starfleet` | Star Trek | 8 | Comm badge chirp, door swishes, turbolift whoosh, transporter shimmer. |
|
||||
| `whoniverse` | Whoniverse | 4 | Lever clunk, TARDIS doors, `who` mechanical telemetry era. |
|
||||
| `bioships` | Bioships | 5 | Bespoke organic telemetry, Vorlon singing crystal, neural bond swells. |
|
||||
| `spacestations` | Space Stations | 6 | Belter telemetry era, Sevastopol sparks, commlock tone, mag-boots. |
|
||||
| `deepspace` | Deep Space | 6 | True hull strain groan, Icarus I beacon, solar roar, cryo-pod sigh. |
|
||||
| `comedy` | Comedy & Adventure | 5 | Tea dispenser gurgle, Beryllium thrum, Omega-13 capacitor whine. |
|
||||
| `outlaw` | Outlaw & Frontier | 5 | Cassette transport clunk, grappler servo whine, true RF static burst. |
|
||||
| `military` | Military Space | 5 | Flak barrage bursts, heavy weapons breech clank, slipstream surge. |
|
||||
| `industrial` | Industrial Space | 5 | Dot-matrix teletype chatter, heavy docking clang, condensation pipe drips. |
|
||||
| `retrofuture` | Retro Future | 5 | Dual-reel tape spooler, continuous heterodyne theremin drone, HAL respiration. |
|
||||
@@ -1,41 +0,0 @@
|
||||
# Existing Settings-Page Core Animations
|
||||
|
||||
This document summarizes the current implementation of the core visualizer card in the settings/control screen's right sidebar (`warp-core-canvas`). It covers all ten universe themes. Findings come from reading the source; they have not been visually verified in a browser. Observation-mode scenes and the separate audio spectrum display are outside this inventory.
|
||||
|
||||
## Theme inventory
|
||||
|
||||
| Theme | Settings card title / overlay | Existing renderer and appearance |
|
||||
| --- | --- | --- |
|
||||
| Starfleet Command | **MATTER / ANTIMATTER CORE** / `INTERMIX CHAMBER // ACTIVE` | `warp-core`: a vertical blue glass intermix chamber in a dark housing, orange injectors at the top and bottom, a white/cyan central reaction glow, 14 horizontal cyan coils, and pale blue plasma particles traveling vertically along a sinusoidal path. Pulses enlarge and brighten the central glow and particles, strengthen the coils, and launch extra particles from the center in both directions. |
|
||||
| The Whoniverse | **CENTRAL TIME ROTOR COLUMN** / `VORTEX ROTOR // OPERATIONAL` | `time-rotor`: a cyan glass cylinder with brass/gold end collars. An inner assembly of three illuminated rods and four gold elliptical rings rises and falls continuously. A white/cyan glow follows the moving assembly, while cyan particles drift through a winding path around it. Audio pulses increase the glow and particle emphasis; the vertical travel runs on its own animation phase. All seven TARDIS console presets share this drawing. |
|
||||
| Industrial Space | **HEAVY FUSION REACTOR** / `CONTAINMENT CORE // ACTIVE` | `industrial-reactor`: a dark brown cast-iron-style vertical housing, top/bottom warning-band rectangles, a white/yellow/amber central plasma glow, and nine horizontal coil clamps alternating amber and brown. Pulses expand the glow and increase the clamps' shadow glow. There are no moving mechanical parts or rendered plasma particles in this mode. |
|
||||
| Bioships | **ORGANIC BIO-HEART SAC** / `VASCULAR NEXUS // PULSING` | `bio-heart`: a soft mint/emerald central aura fading into violet, with six curved green neural veins radiating outward. Audio pulses expand the aura and vein reach and increase its brightness. The visible form is an abstract radial glow and veins. Its additional phase-based breathing and vein rotation do not advance while this mode is selected; see the behavior notes below. |
|
||||
| Retro Future | **ASTROGATOR OSCILLOSCOPE** / `ANALOG BEAM // ROTATING` | `retro-oscilloscope`: a circular dark green CRT face with a green rim, crosshairs, and a glowing pale green closed waveform. Forty-eight points form a ring whose radius is distorted by live audio time-domain samples; pulse energy increases the distortion scale. Despite the overlay, its angular phase does not advance in this mode. Drawing requires an initialized audio analyser. |
|
||||
| Military Space | **DRADIS TACTICAL COMBAT CORE** / `CONDITION ONE // ARMED` | `warp-core`: the same blue/cyan chamber, orange injectors, central glow, 14 coils, and traveling particles used by Starfleet. The title and overlay provide the military identity; the core renderer contains no dedicated DRADIS or tactical geometry. |
|
||||
| Deep Space | **GRAVITY SINGULARITY CORE** / `EVENT HORIZON // LOCKED` | `singularity-core`: a flattened elliptical indigo/blue accretion glow behind a small black circular event horizon, outlined in indigo. Pulses change disk opacity and the event horizon's halo strength. Disk and sphere sizes remain fixed. The disk has a phase-based rotation transform, but its phase does not advance in this mode, so it does not continuously rotate. |
|
||||
| Outlaw / Frontier | **MODIFIED SUB-LIGHT REACTOR** / `KINETIC THRUSTERS // PRIMED` | `warp-core`: the same Starfleet-style blue/cyan chamber animation, including orange injectors, pulsing central glow, 14 coils, and plasma particles. There is no separate sub-light reactor or thruster animation. |
|
||||
| Space Stations | **CENTRIFUGAL ROTATION CORE** / `STATION ROTOR // DOCKED` | `industrial-reactor`: the same amber glow, dark housing, warning-band rectangles, and nine coil clamps as Industrial Space. Pulses change glow size and coil illumination. Despite its title, no centrifugal rotor or station rotation is drawn. |
|
||||
| Comedy / Adventure | **IMPROBABILITY CORE** / `REALITY FIELD // SHIFTING` | `warp-core`: the same Starfleet-style blue/cyan chamber, orange injectors, 14 coils, central glow, and traveling particles. There is no dedicated improbability distortion or shifting reality animation. |
|
||||
|
||||
## Shared behavior and implementation details
|
||||
|
||||
- **Ten themes use six renderers.** Starfleet, Military, Outlaw, and Comedy share `warp-core`. Industrial and Space Stations share `industrial-reactor`. Whoniverse, Bioships, Retro Future, and Deep Space each select one of the other four renderers.
|
||||
- **Universe selection chooses the drawing.** `switchUniverse` applies the universe's theme class, updates the card text, and calls `visualizer.setMode(...)`. Selecting a ship/room preset changes sound parameters but does not select another core renderer. Starfleet's TNG, Voyager, Defiant, DS9, TOS, and NX preset theme values do not produce distinct core drawings in the current settings-page wiring.
|
||||
- **Most pulse responses come from a synth callback.** Each `WarpCoreSynth` pulse resets visual `pulseEnergy` to `1.0` and adds six particles at the reaction center. Energy decays by a factor of `0.94` per animation frame, down to a floor of `0.15`. The callback receives pulse phase and duration, but the core visualizer does not use either value to shape the flash. BPM affects callback timing; the visual flash does not reproduce each preset's detailed audio envelope.
|
||||
- **Only the retro waveform directly samples the audio signal.** Its ring deformation uses the analyser's time-domain data. The other core drawings use pulse energy and/or procedural particle movement rather than measured audio amplitude.
|
||||
- **The TARDIS owns the only advancing phase.** `rotorPhase` starts at zero and advances by `0.038` per frame only inside `renderTimeRotor()`. Bio-heart breathing/vein rotation, retro waveform orientation, and singularity disk rotation read that same value without advancing it. Switching away from the TARDIS preserves its last phase, leaving those other renderers at a fixed phase until TARDIS mode advances it again.
|
||||
- **Animation continues independently of master playback.** The visualizer starts a recurring `requestAnimationFrame` loop at initialization. When synth pulses stop, energy settles to its minimum. Warp particles and TARDIS travel continue, while the pulse-only reactor/heart/singularity effects settle. The retro drawing depends on analyser availability and data.
|
||||
- **Particles are shared, but only two modes draw them.** The initial pool contains 36 particles. Warp-core and time-rotor modes move, recycle, and trim them. Other modes still receive pulse-created particles through the shared callback but do not render or trim that pool. `setMode` only changes the mode; it does not reset particles, energy, or phase.
|
||||
- **Core colors are defined in JavaScript.** The canvas renderers use literal color values rather than reading each theme's CSS palette. Themes sharing a renderer therefore share its internal colors and geometry, even though their surrounding interface styling and labels differ.
|
||||
- **Motion and decay are frame-based.** Particle movement, TARDIS phase advancement, and pulse decay use per-frame increments rather than elapsed time, so their real-time speed can vary with rendering frame rate.
|
||||
|
||||
## Source references
|
||||
|
||||
- `index.html`: core visualizer card, title, label, and canvas.
|
||||
- `js/config.js`: `UniverseRegistry` theme-to-renderer assignments and ship/room presets.
|
||||
- `js/app.js`: visualizer construction, `switchUniverse`, title map, `selectPreset`, and visualizer initialization.
|
||||
- `js/visualizer.js`: pulse callback, mode selection, render loop, particle handling, and all six core renderers.
|
||||
- `js/audio.js`: `WarpCoreSynth.startPulseLoop()` and `scheduleNextPulse()` supply pulse timing.
|
||||
- `css/style.css`: shared canvas wrapper and 240px core wrapper height.
|
||||
|
||||
This is an inventory of existing behavior. No application code changes or proposed redesigns are included.
|
||||
+2432
-2373
File diff suppressed because it is too large
Load Diff
+21695
File diff suppressed because it is too large
Load Diff
Vendored
+18483
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+99
-6
@@ -15,7 +15,7 @@
|
||||
<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>
|
||||
<title>Sci-Fi Ambience Generator - Publish Build v14co</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body class="theme-lcars-tng">
|
||||
@@ -37,7 +37,7 @@
|
||||
<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.5</span>
|
||||
<span id="header-matrix-label">STARFLIGHT SOUND MATRIX v2.9</span>
|
||||
</div>
|
||||
<div class="lcars-pill-end"></div>
|
||||
</header>
|
||||
@@ -84,9 +84,10 @@
|
||||
<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>
|
||||
<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">
|
||||
@@ -148,6 +149,23 @@
|
||||
</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>
|
||||
|
||||
@@ -261,6 +279,79 @@
|
||||
</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>
|
||||
|
||||
<!-- Local MIDI Control -->
|
||||
<div class="panel-card midi-card" id="midi-card">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">LOCAL MIDI CONTROL</span>
|
||||
<span class="panel-sub">WEB MIDI / MIDI LEARN</span>
|
||||
</div>
|
||||
<div class="midi-device-line" id="midi-device-status">MIDI OFFLINE - permission not yet requested.</div>
|
||||
<div class="ai-control-grid">
|
||||
<button class="btn-lcars-pill" id="btn-midi-enable">ENABLE MIDI</button>
|
||||
<button class="btn-lcars-pill" id="btn-midi-clear">CLEAR MAP</button>
|
||||
<select class="span-2" id="midi-target-select" aria-label="MIDI learn target"></select>
|
||||
<button class="btn-lcars-pill span-2" id="btn-midi-learn">MIDI LEARN: SELECT TARGET, THEN MOVE CONTROL</button>
|
||||
</div>
|
||||
<div class="midi-bindings" id="midi-bindings">NO MIDI BINDINGS</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">
|
||||
@@ -291,7 +382,7 @@
|
||||
|
||||
|
||||
<!-- OBSERVATION: passive casting / display mode. Click background to return. -->
|
||||
<section class="observation-overlay" id="observation-overlay" aria-hidden="true" aria-label="Observation display">
|
||||
<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>
|
||||
|
||||
@@ -363,6 +454,8 @@
|
||||
<script src="js/visualizer.js"></script>
|
||||
<script src="js/observation-bezels.js"></script>
|
||||
<script src="js/observation-engine.js"></script>
|
||||
<script src="js/midi.js"></script>
|
||||
<script src="js/generative-experience.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,16 +7,19 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 1. Initialize Audio Subsystems
|
||||
const audioManager = new AudioManager();
|
||||
const fncSystem = new FuturisticNoiceCancellation(audioManager);
|
||||
fncSystem.bindUI();
|
||||
window.fncSystem = fncSystem;
|
||||
const hullDrone = new HullDroneSynth(audioManager);
|
||||
const warpCore = new WarpCoreSynth(audioManager);
|
||||
const lifeSupport = new LifeSupportSynth(audioManager);
|
||||
const telemetry = new TelemetrySynth(audioManager);
|
||||
const alerts = new AlertSynth(audioManager);
|
||||
const whoniverseAudio = new WhataverseAudioSynth(audioManager);
|
||||
const whataverseAudio = new WhataverseAudioSynth(audioManager);
|
||||
const expandedAudio = new ExpandedSciFiAudioSynth(audioManager);
|
||||
const engineTransitions = new EngineTransitionSynth(audioManager);
|
||||
window.expandedAudio = expandedAudio;
|
||||
window.whoniverseAudio = whoniverseAudio;
|
||||
window.whataverseAudio = whataverseAudio;
|
||||
window.engineTransitions = engineTransitions;
|
||||
const visualizer = new StarshipVisualizer(audioManager, warpCore);
|
||||
const observationEngine = new ObservationEngine(audioManager, warpCore, alerts, hullDrone, lifeSupport);
|
||||
@@ -94,6 +97,117 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const timerDisplay = document.getElementById('timer-display');
|
||||
const timerButtons = document.querySelectorAll('.btn-timer');
|
||||
|
||||
// XZBT Generative Experience subsystem. Deterministic engine remains authoritative.
|
||||
const generativeExperience = new XZBTGenerativeExperience({
|
||||
getUniverse: () => UniverseRegistry[activeUniverseId],
|
||||
getPreset: () => UniverseRegistry[activeUniverseId]?.presets?.[activePresetId] || null,
|
||||
isPlaying: () => isPlaying,
|
||||
isObservation: () => observationActive,
|
||||
audioManager,
|
||||
onGenerated: (line, state) => {
|
||||
const ticker = document.getElementById('observation-ticker-text');
|
||||
if (ticker && observationActive) ticker.textContent = line.toUpperCase();
|
||||
window.dispatchEvent(new CustomEvent('xzbt:generated-content', { detail: { line, state } }));
|
||||
}
|
||||
});
|
||||
generativeExperience.bindUI();
|
||||
window.generativeExperience = generativeExperience;
|
||||
|
||||
// Unified semantic control bus: UI, MIDI and future external controls converge here.
|
||||
const controlBus = new XZBTControlBus();
|
||||
window.xzbtControlBus = controlBus;
|
||||
const rangeTarget = (id, label) => {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
controlBus.register(id.replace(/^slider-/, ''), {
|
||||
label,
|
||||
type: 'range',
|
||||
min: Number(el.min || 0),
|
||||
max: Number(el.max || 1),
|
||||
step: Number(el.step || 0),
|
||||
apply: v => {
|
||||
el.value = v;
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
});
|
||||
};
|
||||
rangeTarget('slider-master-vol', 'Master Volume');
|
||||
rangeTarget('slider-hull-vol', 'Hull Level');
|
||||
rangeTarget('slider-hull-freq', 'Hull Base Tone');
|
||||
rangeTarget('slider-hull-cutoff', 'Hull Damping Cutoff');
|
||||
rangeTarget('slider-warp-vol', 'Core / Drive Level');
|
||||
rangeTarget('slider-warp-bpm', 'Core / Drive Pulse Rate');
|
||||
rangeTarget('slider-warp-carrier', 'Core / Drive Carrier');
|
||||
rangeTarget('slider-air-vol', 'Environmental Level');
|
||||
rangeTarget('slider-air-cutoff', 'Environmental Cutoff');
|
||||
rangeTarget('slider-telemetry-vol', 'Telemetry Level');
|
||||
rangeTarget('slider-telemetry-density', 'Telemetry Density');
|
||||
rangeTarget('ai-robot-amount', 'Speech Automation / Robot');
|
||||
rangeTarget('slider-fnc-level', 'FNC Level');
|
||||
rangeTarget('slider-observation-activity', 'Experience Visual Activity');
|
||||
controlBus.register('master-play', {
|
||||
label: 'Engage / Full Stop',
|
||||
type: 'action',
|
||||
apply: () => document.getElementById('btn-master-play')?.click()
|
||||
});
|
||||
controlBus.register('fnc-enable', {
|
||||
label: 'FNC Enable / Bypass',
|
||||
type: 'action',
|
||||
apply: () => document.getElementById('btn-fnc-enable')?.click()
|
||||
});
|
||||
controlBus.register('watch-experience', {
|
||||
label: 'Watch Experience',
|
||||
type: 'action',
|
||||
apply: () => document.getElementById('btn-observation')?.click()
|
||||
});
|
||||
controlBus.register('observation-viewport', {
|
||||
label: 'Observation Viewport Toggle',
|
||||
type: 'action',
|
||||
apply: () => document.getElementById('toggle-observation-viewport')?.click()
|
||||
});
|
||||
controlBus.register('red-alert', {
|
||||
label: 'Red Alert Toggle',
|
||||
type: 'action',
|
||||
apply: () => document.getElementById('obs-btn-alert')?.click()
|
||||
});
|
||||
controlBus.register('generate-announcement', {
|
||||
label: 'Generate AI Announcement',
|
||||
type: 'action',
|
||||
apply: () => generativeExperience.generateAmbientMoment('midi-request')
|
||||
});
|
||||
controlBus.register('mute', {
|
||||
label: 'Master Mute',
|
||||
type: 'action',
|
||||
apply: () => document.getElementById('btn-master-mute')?.click()
|
||||
});
|
||||
const midiController = new XZBTMidiController(controlBus);
|
||||
midiController.bindUI();
|
||||
window.xzbtMidiController = midiController;
|
||||
|
||||
// TV presentation controls. The final Cast stream quality remains Chrome-controlled.
|
||||
const tvAspect = document.getElementById('tv-aspect-lock'),
|
||||
tvQuality = document.getElementById('tv-quality'),
|
||||
tvQualityValue = document.getElementById('tv-quality-value');
|
||||
const applyTV = () => {
|
||||
document.body.classList.toggle('xzbt-tv-aspect', !!tvAspect?.checked);
|
||||
document.body.classList.toggle('xzbt-tv-letterbox', !!tvAspect?.checked);
|
||||
window.xzbtRenderScale = Number(tvQuality?.value || 1);
|
||||
if (tvQualityValue) {
|
||||
const labels = {
|
||||
0.667: '720P TARGET',
|
||||
1: '1080P TARGET',
|
||||
1.333: '1440P TARGET',
|
||||
2: '2160P TARGET'
|
||||
};
|
||||
tvQualityValue.textContent = labels[tvQuality.value] || '1080P TARGET';
|
||||
}
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
};
|
||||
tvAspect?.addEventListener('change', applyTV);
|
||||
tvQuality?.addEventListener('change', applyTV);
|
||||
applyTV();
|
||||
|
||||
// 3. Universe Management & Switching
|
||||
function initUniverseDropdown() {
|
||||
universeDropdown.innerHTML = '';
|
||||
@@ -124,6 +238,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
activeUniverseId = universeId;
|
||||
universeDropdown.classList.remove('show');
|
||||
if (window.generativeExperience) window.generativeExperience.onThemeChanged();
|
||||
if (window.fncSystem) window.fncSystem.setTheme(universeId);
|
||||
|
||||
// Update Dropdown UI active class
|
||||
document.querySelectorAll('.universe-item').forEach(el => {
|
||||
@@ -133,6 +249,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// Update Header and Theme Class
|
||||
headerUniverseLabel.textContent = universe.shortCode;
|
||||
document.body.className = universe.themeClass;
|
||||
// Assigning className wholesale drops the TV presentation classes, so re-apply them.
|
||||
applyTV();
|
||||
headerMatrixLabel.textContent = universe.headerTitle;
|
||||
|
||||
// Custom titles per universe
|
||||
@@ -143,7 +261,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
visLabel: 'INTERMIX CHAMBER // ACTIVE',
|
||||
soundboard: 'LCARD KEYPAD'
|
||||
},
|
||||
'whoniverse': {
|
||||
'whataverse': {
|
||||
sidebar: 'TARDIX CONSOLES',
|
||||
visTitle: 'CENTRAL TIME ROTOR COLUMN',
|
||||
visLabel: 'VORTEX ROTOR // OPERATIONAL',
|
||||
@@ -288,7 +406,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// re-engages the ambience matching the currently selected preset.
|
||||
function startPresetAmbience(presetId) {
|
||||
expandedAudio.stopAllLoops();
|
||||
whoniverseAudio.stopAmbientLoops();
|
||||
whataverseAudio.stopAmbientLoops();
|
||||
|
||||
// Legacy Phase 1 loops, kept on their original presets
|
||||
if (presetId === 'sta-sevastopol') {
|
||||
@@ -328,6 +446,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
activePresetId = presetId;
|
||||
window.activePresetId = presetId;
|
||||
if (window.fncSystem) window.fncSystem.resetLearning('preset change');
|
||||
|
||||
document.querySelectorAll('.preset-btn').forEach(b => {
|
||||
b.classList.toggle('active', b.getAttribute('data-preset-id') === presetId);
|
||||
@@ -346,10 +465,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
syncSlidersToSynthParams();
|
||||
|
||||
expandedAudio.stopAllLoops();
|
||||
whoniverseAudio.stopAmbientLoops();
|
||||
whataverseAudio.stopAmbientLoops();
|
||||
if (isPlaying) {
|
||||
startPresetAmbience(presetId);
|
||||
if (activeUniverseId === 'whoniverse') whoniverseAudio.synthesizeDematSwitch();
|
||||
if (activeUniverseId === 'whataverse') whataverseAudio.synthesizeDematSwitch();
|
||||
else if (activeUniverseId === 'industrial') expandedAudio.synthesizeDockingClamp();
|
||||
else telemetry.synthesizeLCARDSingleChirp();
|
||||
}
|
||||
@@ -399,6 +518,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (!isPlaying) {
|
||||
isTransitioning = true;
|
||||
await audioManager.resume();
|
||||
fncSystem.ensureGraph();
|
||||
if (fncSystem.enabled) fncSystem.start();
|
||||
// Prepare the configured local LLM and neural voice in parallel with startup.
|
||||
generativeExperience.prepareExperience().catch(err => console.warn('Generative preload failed', err));
|
||||
|
||||
// Flashing state while engaging
|
||||
btnPlay.classList.add('engaging');
|
||||
@@ -410,7 +533,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
engineTransitions.playStartup(engineProfile, ENGINE_TRANSITION_SECS);
|
||||
|
||||
// Acoustic chime / chirp feedback
|
||||
if (activeUniverseId === 'whoniverse') whoniverseAudio.synthesizeTelepathicChime();
|
||||
if (activeUniverseId === 'whataverse') whataverseAudio.synthesizeTelepathicChime();
|
||||
else if (activeUniverseId === 'bioships') expandedAudio.synthesizeStarburst();
|
||||
else telemetry.synthesizeLCARDDoubleChirp();
|
||||
|
||||
@@ -434,6 +557,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (activePresetId) {
|
||||
startPresetAmbience(activePresetId);
|
||||
}
|
||||
if (generativeExperience.ui.auto?.checked) generativeExperience.scheduleNext();
|
||||
}, ENGINE_TRANSITION_SECS * 1000);
|
||||
|
||||
} else {
|
||||
@@ -448,7 +572,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// Layered & Staged Procedural Shutdown Script for this Profile
|
||||
engineTransitions.playShutdown(engineProfile, ENGINE_TRANSITION_SECS);
|
||||
|
||||
if (activeUniverseId === 'whoniverse') whoniverseAudio.synthesizeLeverClunk();
|
||||
if (activeUniverseId === 'whataverse') whataverseAudio.synthesizeLeverClunk();
|
||||
else telemetry.synthesizeLCARDDoubleChirp();
|
||||
|
||||
// Stop engine sounds with smooth spool-down fade
|
||||
@@ -457,10 +581,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
lifeSupport.stop(ENGINE_TRANSITION_SECS);
|
||||
telemetry.stop();
|
||||
expandedAudio.stopAllLoops();
|
||||
whoniverseAudio.stopAmbientLoops();
|
||||
whataverseAudio.stopAmbientLoops();
|
||||
alerts.stopAlert();
|
||||
whoniverseAudio.stopCloisterBell();
|
||||
whataverseAudio.stopCloisterBell();
|
||||
fncSystem.hold('alert', false);
|
||||
resetAlertButtons();
|
||||
generativeExperience.stop();
|
||||
fncSystem.resetLearning('full stop');
|
||||
fncSystem.stop();
|
||||
|
||||
setTimeout(() => {
|
||||
isPlaying = false;
|
||||
@@ -597,6 +725,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
function handleUniverseEvent(type, btnElement) {
|
||||
fncSystem.holdFor(6500, 'event');
|
||||
switch (type) {
|
||||
case 'warp':
|
||||
case 'quantum':
|
||||
@@ -608,39 +737,45 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
case 'scramble':
|
||||
if (alerts.activeAlert === 'red') {
|
||||
alerts.stopAlert();
|
||||
fncSystem.hold('alert', false);
|
||||
resetAlertButtons();
|
||||
} else {
|
||||
resetAlertButtons();
|
||||
btnElement.classList.add('active-alert');
|
||||
alerts.triggerRedAlert('tng');
|
||||
fncSystem.hold('alert', true);
|
||||
}
|
||||
break;
|
||||
case 'alert-yellow':
|
||||
if (alerts.activeAlert === 'yellow') {
|
||||
alerts.stopAlert();
|
||||
fncSystem.hold('alert', false);
|
||||
resetAlertButtons();
|
||||
} else {
|
||||
resetAlertButtons();
|
||||
btnElement.classList.add('active-alert');
|
||||
alerts.triggerYellowAlert();
|
||||
fncSystem.hold('alert', true);
|
||||
}
|
||||
break;
|
||||
case 'demat':
|
||||
whoniverseAudio.synthesizeDematCycle(4);
|
||||
whataverseAudio.synthesizeDematCycle(4);
|
||||
visualizer.spawnWarpPulses();
|
||||
break;
|
||||
case 'vortex':
|
||||
whoniverseAudio.synthesizeDematCycle(1);
|
||||
whataverseAudio.synthesizeDematCycle(1);
|
||||
visualizer.spawnWarpPulses();
|
||||
break;
|
||||
case 'cloister':
|
||||
if (whoniverseAudio.activeCloister) {
|
||||
whoniverseAudio.stopCloisterBell();
|
||||
if (whataverseAudio.activeCloister) {
|
||||
whataverseAudio.stopCloisterBell();
|
||||
fncSystem.hold('alert', false);
|
||||
resetAlertButtons();
|
||||
} else {
|
||||
resetAlertButtons();
|
||||
btnElement.classList.add('active-alert');
|
||||
whoniverseAudio.triggerCloisterBell();
|
||||
whataverseAudio.triggerCloisterBell();
|
||||
fncSystem.hold('alert', true);
|
||||
}
|
||||
break;
|
||||
case 'epstein':
|
||||
@@ -654,7 +789,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
expandedAudio.synthesizeDockingClamp();
|
||||
break;
|
||||
case 'vent':
|
||||
whoniverseAudio.synthesizeFastReturn();
|
||||
whataverseAudio.synthesizeFastReturn();
|
||||
break;
|
||||
case 'starburst':
|
||||
case 'biodefense':
|
||||
@@ -705,9 +840,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
telemetry.synthesizeLCARDSingleChirp();
|
||||
break;
|
||||
}
|
||||
generativeExperience.describeExistingEvent(type);
|
||||
}
|
||||
|
||||
function handleSoundboardTrigger(id) {
|
||||
fncSystem.holdFor(2800, 'soundboard');
|
||||
const universe = UniverseRegistry[activeUniverseId];
|
||||
const preset = universe && universe.presets ? universe.presets[activePresetId] : null;
|
||||
const era = preset ? preset.era : 'tng';
|
||||
@@ -745,27 +882,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// Whataverse
|
||||
case 'btn-sonic':
|
||||
whoniverseAudio.synthesizeSonicScrewdriver();
|
||||
whataverseAudio.synthesizeSonicScrewdriver();
|
||||
break;
|
||||
case 'btn-lever-clunk':
|
||||
whoniverseAudio.synthesizeLeverClunk();
|
||||
whataverseAudio.synthesizeLeverClunk();
|
||||
break;
|
||||
case 'btn-tardis-door':
|
||||
whoniverseAudio.synthesizeTARDIXDoor();
|
||||
case 'btn-tardix-door':
|
||||
whataverseAudio.synthesizeTARDIXDoor();
|
||||
break;
|
||||
case 'btn-fast-return':
|
||||
whoniverseAudio.synthesizeFastReturn();
|
||||
whataverseAudio.synthesizeFastReturn();
|
||||
break;
|
||||
case 'btn-demat-switch':
|
||||
case 'btn-relay-click':
|
||||
whoniverseAudio.synthesizeDematSwitch();
|
||||
whataverseAudio.synthesizeDematSwitch();
|
||||
break;
|
||||
case 'btn-telepathic':
|
||||
case 'btn-telepathic-chime':
|
||||
whoniverseAudio.synthesizeTelepathicChime();
|
||||
whataverseAudio.synthesizeTelepathicChime();
|
||||
break;
|
||||
case 'btn-scanner':
|
||||
whoniverseAudio.synthesizeDematSwitch();
|
||||
whataverseAudio.synthesizeDematSwitch();
|
||||
telemetry.synthesizeSensorSweep();
|
||||
break;
|
||||
|
||||
@@ -786,7 +923,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
expandedAudio.synthesizeStationMedicalPing();
|
||||
break;
|
||||
case 'btn-tram-gong':
|
||||
whoniverseAudio.synthesizeCloisterStrike();
|
||||
whataverseAudio.synthesizeCloisterStrike();
|
||||
break;
|
||||
|
||||
// Deep Space
|
||||
@@ -857,10 +994,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
expandedAudio.synthesizeFabricatorShimmer();
|
||||
break;
|
||||
case 'btn-scanner-activate':
|
||||
whoniverseAudio.synthesizeScannerActivate();
|
||||
whataverseAudio.synthesizeScannerActivate();
|
||||
break;
|
||||
case 'btn-type-clatter':
|
||||
whoniverseAudio.synthesizeTypeClatter();
|
||||
whataverseAudio.synthesizeTypeClatter();
|
||||
break;
|
||||
case 'btn-vorlon-song':
|
||||
expandedAudio.synthesizeVorlanSingingSwell();
|
||||
@@ -1079,7 +1216,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
function obsThemeSplineScale(theme) {
|
||||
return ({
|
||||
starfleet: 0.60,
|
||||
whoniverse: 1.05,
|
||||
whataverse: 1.05,
|
||||
industrial: 0.48,
|
||||
bioships: 1.28,
|
||||
retrofuture: 0.72,
|
||||
@@ -1439,7 +1576,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
</g>`;
|
||||
}
|
||||
|
||||
if (theme === 'whoniverse') {
|
||||
if (theme === 'whataverse') {
|
||||
const glyph = (char,color,size) => `<text x="0" y="0" text-anchor="middle" dominant-baseline="middle" fill="${color}" font-size="${size}" font-family="monospace">${char}</text>`;
|
||||
return `${defs}
|
||||
${obsOrbitMarkup('who-glyph-1', glyph('◎','#00e5ff',38), {cx:800,cy:455,rx:335,ry:160,duration:23000,phase:.1,scale0:.55,scale1:1.2,minActivity:.05})}
|
||||
@@ -2413,7 +2550,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
const generator = {
|
||||
starfleet: generateStarflightActivity,
|
||||
whoniverse: generateWhataverseActivity,
|
||||
whataverse: generateWhataverseActivity,
|
||||
industrial: generateIndustrialActivity,
|
||||
bioships: generateBioshipActivity,
|
||||
retrofuture: generateRetroActivity,
|
||||
@@ -2546,7 +2683,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
const observationTextVocabulary = {
|
||||
starfleet: ['NOMINAL','PASSIVE','SCANNING','CORRELATING','TRACKING','VERIFIED','STABLE'],
|
||||
whoniverse: ['VORTEX LOCK','CONVERGING','DRIFTING','RECURSIVE','RESOLVING','TEMPORAL HOLD'],
|
||||
whataverse: ['VORTEX LOCK','CONVERGING','DRIFTING','RECURSIVE','RESOLVING','TEMPORAL HOLD'],
|
||||
industrial: ['SECURE FEED','MONITORING','SIGNAL HOLD','CYCLING','LOCAL CHECK','STABLE'],
|
||||
bioships: ['QUIESCENT','RESPONDING','ADAPTING','RESONANT','SYNCHRONIZED','PULSING'],
|
||||
retrofuture: ['READY','READ','PROCESS','HOLD','TRACK','STABLE'],
|
||||
@@ -3901,6 +4038,62 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
function renderObservationOutlaw() {
|
||||
const stars = makeStars(115, 1700, 900, '#fbcfe8', 1710);
|
||||
|
||||
const gaugeArc = (r, startDeg = -140, endDeg = -40) => {
|
||||
const sa = startDeg * Math.PI / 180;
|
||||
const ea = endDeg * Math.PI / 180;
|
||||
const sx = (Math.cos(sa) * r).toFixed(1);
|
||||
const sy = (Math.sin(sa) * r).toFixed(1);
|
||||
const ex = (Math.cos(ea) * r).toFixed(1);
|
||||
const ey = (Math.sin(ea) * r).toFixed(1);
|
||||
return `M ${sx} ${sy} A ${r} ${r} 0 0 1 ${ex} ${ey}`;
|
||||
};
|
||||
|
||||
const gaugeTicks = (r, color) => {
|
||||
const ticks = [-140, -115, -90, -65, -40];
|
||||
return ticks.map((deg, idx) => {
|
||||
const a = deg * Math.PI / 180;
|
||||
const ix = Math.cos(a) * (r - 9);
|
||||
const iy = Math.sin(a) * (r - 9);
|
||||
const ox = Math.cos(a) * (r + (idx === 2 ? 10 : 5));
|
||||
const oy = Math.sin(a) * (r + (idx === 2 ? 10 : 5));
|
||||
return `<line x1="${ix.toFixed(1)}" y1="${iy.toFixed(1)}" x2="${ox.toFixed(1)}" y2="${oy.toFixed(1)}" stroke="${color}" stroke-width="${idx === 2 ? 3 : 2}" opacity="${idx === 2 ? '.85' : '.45'}"/>`;
|
||||
}).join('');
|
||||
};
|
||||
|
||||
const outlawGauge = ({ x, y, r, label, sublabel, arcColor, nodeColor, pointerColor, values, dur, begin = '0s', opacity = '.95', drift = false }) => {
|
||||
const tipR = (r * 0.8).toFixed(1);
|
||||
const arcPath = gaugeArc(r);
|
||||
const smallArcPath = gaugeArc(r - 10, -132, -48);
|
||||
return `
|
||||
<g transform="translate(${x} ${y})" opacity="${opacity}">
|
||||
<text x="0" y="-${r + 34}" text-anchor="middle" fill="#fde68a" font-size="14" font-family="monospace" letter-spacing="2">${label}</text>
|
||||
<text x="0" y="-${r + 16}" text-anchor="middle" fill="#f472b6" opacity=".72" font-size="11" font-family="monospace">${sublabel}</text>
|
||||
<path d="${arcPath}" fill="none" stroke="${arcColor}" stroke-width="6" stroke-linecap="round" opacity=".2"/>
|
||||
<path d="${smallArcPath}" fill="none" stroke="${arcColor}" stroke-width="2" stroke-linecap="round" opacity=".72" stroke-dasharray="10 12">
|
||||
<animate attributeName="stroke-dashoffset" values="0;-44" dur="5.8s" repeatCount="indefinite"/>
|
||||
<animate attributeName="opacity" values=".35;.85;.35" dur="4.2s" repeatCount="indefinite"/>
|
||||
</path>
|
||||
${gaugeTicks(r, '#fbcfe8')}
|
||||
<circle cx="0" cy="0" r="9" fill="#1a0b1f" stroke="#8b5cf6" stroke-width="2" opacity=".95"/>
|
||||
<path d="M-${(r*0.92).toFixed(1)} ${(r*0.72).toFixed(1)} H ${(r*0.92).toFixed(1)}" stroke="#f472b6" stroke-width="1.2" opacity=".18"/>
|
||||
<g${drift ? ' class="obs-crt-jump"' : ''}>
|
||||
<g>
|
||||
<animateTransform attributeName="transform" type="rotate" values="${values}" dur="${dur}" repeatCount="indefinite" begin="${begin}"/>
|
||||
<line x1="0" y1="0" x2="0" y2="-${tipR}" stroke="${pointerColor}" stroke-width="3.2" stroke-linecap="round" opacity=".95"/>
|
||||
<circle cx="0" cy="-${tipR}" r="5.5" fill="${nodeColor}" opacity=".95">
|
||||
<animate attributeName="r" values="5.2;7;5.2" dur="2.4s" begin="${begin}" repeatCount="indefinite"/>
|
||||
<animate attributeName="opacity" values=".9;1;.9" dur="2.4s" begin="${begin}" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
<circle cx="0" cy="-${tipR}" r="13" fill="none" stroke="${nodeColor}" stroke-width="1.4" opacity=".32">
|
||||
<animate attributeName="r" values="9;16;9" dur="2.4s" begin="${begin}" repeatCount="indefinite"/>
|
||||
<animate attributeName="opacity" values=".25;.05;.25" dur="2.4s" begin="${begin}" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
</g>
|
||||
</g>
|
||||
</g>`;
|
||||
};
|
||||
|
||||
return svgFrame(`
|
||||
<rect width="1600" height="900" fill="#09040c"/>
|
||||
<g style="animation:obs-drift-left 95s linear infinite" opacity=".55">${stars}</g>
|
||||
@@ -3909,6 +4102,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<path d="M630 390h120v-45M970 390H850v-45M630 540h120v45M970 540H850v45" stroke-width="4"/>
|
||||
<circle cx="800" cy="465" r="95" opacity=".25"/>
|
||||
</g>
|
||||
|
||||
<g transform="translate(120 210)">
|
||||
<g class="obs-crt-jump">
|
||||
<rect width="290" height="170" rx="8" fill="#130919" stroke="#8b5cf6" stroke-width="3"/>
|
||||
@@ -3917,6 +4111,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<circle cx="225" cy="68" r="5" fill="#ec4899" class="obs-blink"/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<g opacity=".94">
|
||||
<text x="800" y="132" text-anchor="middle" fill="#fde68a" font-size="14" font-family="monospace" letter-spacing="3" opacity=".82">FIELD TUNING ARRAY</text>
|
||||
<path d="M350 152H1250" stroke="#f472b6" stroke-width="1.5" opacity=".16" stroke-dasharray="8 10"/>
|
||||
|
||||
${outlawGauge({ x: 450, y: 250, r: 64, label: 'PORT BUS', sublabel: 'FLOAT', arcColor: '#fb923c', nodeColor: '#fde68a', pointerColor: '#f472b6', values: '-24 0 0; 18 0 0; -12 0 0; 22 0 0; -20 0 0; -24 0 0', dur: '8.4s', begin: '0s' })}
|
||||
${outlawGauge({ x: 800, y: 244, r: 76, label: 'JUMP BIAS', sublabel: 'PEG / BOUNCE', arcColor: '#ec4899', nodeColor: '#fb923c', pointerColor: '#fde68a', values: '-6 0 0; 46 0 0; 38 0 0; 44 0 0; 10 0 0; -12 0 0; -6 0 0', dur: '5.8s', begin: '1.2s', drift: true })}
|
||||
${outlawGauge({ x: 1150, y: 250, r: 64, label: 'VECTOR', sublabel: 'CENTER RANGE', arcColor: '#8b5cf6', nodeColor: '#fbcfe8', pointerColor: '#fb923c', values: '-38 0 0; 28 0 0; -14 0 0; 12 0 0; -3 0 0; 0 0 0; 7 0 0; 0 0 0', dur: '7.6s', begin: '.6s' })}
|
||||
</g>
|
||||
|
||||
<g fill="#f472b6" font-family="monospace" opacity=".72">
|
||||
<text x="1190" y="675" font-size="17">ROUTE: MANUAL</text>
|
||||
<text x="1190" y="702" font-size="14">TRANSPONDER: INTERMITTENT</text>
|
||||
@@ -4061,7 +4265,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
function getObservationSvg(universeId) {
|
||||
switch (universeId) {
|
||||
case 'starfleet': return renderObservationStarflight();
|
||||
case 'whoniverse': return renderObservationWhataverse();
|
||||
case 'whataverse': return renderObservationWhataverse();
|
||||
case 'industrial': return renderObservationIndustrial();
|
||||
case 'bioships': return renderObservationBioships();
|
||||
case 'retrofuture': return renderObservationRetro();
|
||||
@@ -4097,6 +4301,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
function enterObservation() {
|
||||
if (observationActive) return;
|
||||
generativeExperience.prepareExperience().catch(err => console.warn('Generative preload failed', err));
|
||||
observationActive = true;
|
||||
observationOverlay.classList.add('active');
|
||||
observationOverlay.setAttribute('aria-hidden', 'false');
|
||||
@@ -4226,7 +4431,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return;
|
||||
}
|
||||
alerts.stopAlert();
|
||||
whoniverseAudio.stopCloisterBell();
|
||||
whataverseAudio.stopCloisterBell();
|
||||
resetAlertButtons();
|
||||
}
|
||||
});
|
||||
|
||||
+561
-54
@@ -180,6 +180,476 @@ class AudioManager {
|
||||
|
||||
window.AudioManager = AudioManager;
|
||||
|
||||
/**
|
||||
* FNC - Futuristic Noice Cancellation v3.7
|
||||
* Deterministic source-level comfort filtering for the three continuous ambience beds.
|
||||
*
|
||||
* FNC OFF: sources play at the operator-selected channel levels.
|
||||
* FNC ON: the user's FNC level (0..1) immediately adds these smooth reductions:
|
||||
* Life Support: 50% -> 100% reduction
|
||||
* Hull Drone: 10% -> 70% reduction
|
||||
* Worp Core: 5% -> 50% reduction
|
||||
*
|
||||
* This is deliberately an overall source-level comfort filter, not a subtle EQ effect.
|
||||
* The mild global spectral watchdog remains only as a secondary refinement and HOLD
|
||||
* pauses that learning without bypassing the deterministic comfort attenuation.
|
||||
*/
|
||||
class FuturisticNoiceCancellation {
|
||||
constructor(audioManager) {
|
||||
this.am = audioManager;
|
||||
this.enabled = true;
|
||||
this.level = 0.65;
|
||||
this.state = 'ACTIVE';
|
||||
this.themeId = 'starfleet';
|
||||
|
||||
// Mild global watchdog, retained as a secondary refinement only.
|
||||
this.analyser = null;
|
||||
this.filters = [];
|
||||
this.connected = false;
|
||||
this.persistence = new Map();
|
||||
this.activeBuckets = [];
|
||||
this.lastIntervention = 0;
|
||||
|
||||
// Deterministic source comfort gains.
|
||||
this.sourceChains = new Map();
|
||||
|
||||
this.timer = null;
|
||||
this.intervalMs = 1000;
|
||||
this.holds = new Set();
|
||||
this.holdTimers = new Map();
|
||||
this.ui = {};
|
||||
this.storageKey = 'xzbt-fnc-v33';
|
||||
|
||||
// Used only by the optional global watchdog.
|
||||
this.themeProfiles = {
|
||||
starfleet: { protected: [[35, 180]], bias: 1.0 },
|
||||
whataverse: {
|
||||
protected: [
|
||||
[35, 210],
|
||||
[260, 520]
|
||||
],
|
||||
bias: 0.9
|
||||
},
|
||||
industrial: { protected: [[25, 170]], bias: 1.18 },
|
||||
bioships: {
|
||||
protected: [
|
||||
[20, 240],
|
||||
[280, 520]
|
||||
],
|
||||
bias: 0.78
|
||||
},
|
||||
retrofuture: {
|
||||
protected: [
|
||||
[30, 150],
|
||||
[330, 520]
|
||||
],
|
||||
bias: 1.04
|
||||
},
|
||||
military: { protected: [[25, 190]], bias: 1.1 },
|
||||
deepspace: { protected: [[20, 170]], bias: 0.92 },
|
||||
outlaw: { protected: [[25, 220]], bias: 0.9 },
|
||||
spacestations: {
|
||||
protected: [
|
||||
[25, 190],
|
||||
[260, 430]
|
||||
],
|
||||
bias: 1.0
|
||||
},
|
||||
comedy: { protected: [[30, 180]], bias: 0.86 }
|
||||
};
|
||||
}
|
||||
|
||||
bindUI() {
|
||||
const g = id => document.getElementById(id);
|
||||
this.ui = {
|
||||
button: g('btn-fnc-enable'),
|
||||
slider: g('slider-fnc-level'),
|
||||
value: g('val-fnc-level'),
|
||||
state: g('fnc-state'),
|
||||
detail: g('fnc-detail')
|
||||
};
|
||||
this.load();
|
||||
if (this.ui.button)
|
||||
this.ui.button.addEventListener('click', () => this.setEnabled(!this.enabled));
|
||||
if (this.ui.slider)
|
||||
this.ui.slider.addEventListener('input', () => this.setLevel(Number(this.ui.slider.value)));
|
||||
this.syncUI();
|
||||
}
|
||||
|
||||
load() {
|
||||
try {
|
||||
let v = JSON.parse(localStorage.getItem(this.storageKey) || 'null');
|
||||
if (!v) v = JSON.parse(localStorage.getItem('xzbt-fnc-v31') || 'null');
|
||||
if (!v) v = JSON.parse(localStorage.getItem('xzbt-fnc-v30') || 'null');
|
||||
if (v) {
|
||||
if (typeof v.enabled === 'boolean') this.enabled = v.enabled;
|
||||
if (Number.isFinite(v.level)) this.level = Math.max(0, Math.min(1, v.level));
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
save() {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
this.storageKey,
|
||||
JSON.stringify({ enabled: this.enabled, level: this.level })
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
ensureGraph() {
|
||||
if (!this.am?.isInitialized || !this.am.ctx || !this.am.compressor || !this.am.masterGain)
|
||||
return false;
|
||||
if (this.connected) return true;
|
||||
const ctx = this.am.ctx;
|
||||
this.analyser = ctx.createAnalyser();
|
||||
this.analyser.fftSize = 4096;
|
||||
this.analyser.smoothingTimeConstant = 0.93;
|
||||
this.analyser.minDecibels = -110;
|
||||
this.analyser.maxDecibels = -10;
|
||||
this.filters = [];
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const f = ctx.createBiquadFilter();
|
||||
f.type = 'peaking';
|
||||
f.frequency.value = 1200;
|
||||
f.Q.value = 5;
|
||||
f.gain.value = 0;
|
||||
this.filters.push(f);
|
||||
}
|
||||
try {
|
||||
this.am.compressor.disconnect(this.am.masterGain);
|
||||
} catch (_) {}
|
||||
this.am.compressor.connect(this.analyser);
|
||||
let node = this.analyser;
|
||||
this.filters.forEach(f => {
|
||||
node.connect(f);
|
||||
node = f;
|
||||
});
|
||||
node.connect(this.am.masterGain);
|
||||
this.connected = true;
|
||||
this.start();
|
||||
return true;
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.timer || !this.enabled) return;
|
||||
this.timer = setInterval(() => this.tick(), this.intervalMs);
|
||||
}
|
||||
stop() {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
setEnabled(on) {
|
||||
this.enabled = !!on;
|
||||
this.save();
|
||||
if (this.enabled) {
|
||||
this.ensureGraph();
|
||||
this.start();
|
||||
this.applySourceConditioning(false);
|
||||
this.setState(this.holds.size ? 'HOLD' : 'ACTIVE');
|
||||
} else {
|
||||
this.stop();
|
||||
this.releaseAll(1.5);
|
||||
this.applySourceConditioning(false);
|
||||
this.setState('OFF');
|
||||
}
|
||||
this.syncUI();
|
||||
}
|
||||
|
||||
setLevel(v) {
|
||||
this.level = Math.max(0, Math.min(1, Number(v) || 0));
|
||||
this.save();
|
||||
this.applySourceConditioning(false);
|
||||
this.applyCurrentDepths();
|
||||
this.updateOperationalState();
|
||||
this.syncUI();
|
||||
}
|
||||
|
||||
setTheme(id) {
|
||||
this.themeId = this.themeProfiles[id] ? id : 'starfleet';
|
||||
this.resetLearning('theme change');
|
||||
this.applySourceConditioning(false);
|
||||
}
|
||||
|
||||
resetLearning(reason = 'reset') {
|
||||
this.persistence.clear();
|
||||
this.activeBuckets = [];
|
||||
this.lastIntervention = 0;
|
||||
this.releaseAll(1.5);
|
||||
if (this.enabled) this.setState(this.holds.size ? 'HOLD' : 'ACTIVE');
|
||||
if (this.ui.detail)
|
||||
this.ui.detail.textContent = `FNC comfort filter remains applied. Adaptive watchdog memory reset: ${reason}.`;
|
||||
}
|
||||
|
||||
hold(reason = 'transient', on = true) {
|
||||
if (on) {
|
||||
this.holds.add(reason);
|
||||
this.setState(this.enabled ? 'HOLD' : 'OFF');
|
||||
} else {
|
||||
this.holds.delete(reason);
|
||||
if (this.enabled) this.updateOperationalState();
|
||||
}
|
||||
}
|
||||
|
||||
holdFor(ms = 4000, reason = 'transient') {
|
||||
this.hold(reason, true);
|
||||
if (this.holdTimers.has(reason)) clearTimeout(this.holdTimers.get(reason));
|
||||
this.holdTimers.set(
|
||||
reason,
|
||||
setTimeout(() => {
|
||||
this.holdTimers.delete(reason);
|
||||
this.hold(reason, false);
|
||||
}, ms)
|
||||
);
|
||||
}
|
||||
|
||||
// Direct multiplier remaining after FNC reduction.
|
||||
// level 0 -> Life 0.50, Hull 0.90, Worp 0.95
|
||||
// level 1 -> Life 0.00, Hull 0.30, Worp 0.50
|
||||
sourceMultiplier(name) {
|
||||
if (!this.enabled) return 1;
|
||||
const x = this.level;
|
||||
if (name === 'lifeSupport') return Math.max(0, 0.5 - 0.5 * x);
|
||||
if (name === 'hull') return Math.max(0, 0.9 - 0.6 * x);
|
||||
if (name === 'worp') return Math.max(0, 0.95 - 0.45 * x);
|
||||
return 1;
|
||||
}
|
||||
|
||||
sourceReductionPercent(name) {
|
||||
return Math.round((1 - this.sourceMultiplier(name)) * 100);
|
||||
}
|
||||
|
||||
// Route each continuous ambience source through a dedicated comfort gain.
|
||||
connectSource(name, sourceNode, paramsGetter = () => ({})) {
|
||||
if (!sourceNode || !this.am?.ctx || !this.am?.compressor) {
|
||||
if (sourceNode && this.am?.compressor) sourceNode.connect(this.am.compressor);
|
||||
return null;
|
||||
}
|
||||
this.ensureGraph();
|
||||
this.detachSource(name);
|
||||
const ctx = this.am.ctx;
|
||||
const comfortGain = ctx.createGain();
|
||||
comfortGain.gain.setValueAtTime(this.sourceMultiplier(name), ctx.currentTime);
|
||||
sourceNode.connect(comfortGain);
|
||||
comfortGain.connect(this.am.compressor);
|
||||
const chain = { name, sourceNode, paramsGetter, comfortGain, nodes: [comfortGain] };
|
||||
this.sourceChains.set(name, chain);
|
||||
this.applySourceConditioning(true);
|
||||
this.updateOperationalState();
|
||||
return chain;
|
||||
}
|
||||
|
||||
detachSource(name) {
|
||||
const chain = this.sourceChains.get(name);
|
||||
if (!chain) return;
|
||||
try {
|
||||
chain.sourceNode?.disconnect();
|
||||
} catch (_) {}
|
||||
for (const n of chain.nodes || []) {
|
||||
try {
|
||||
n.disconnect();
|
||||
} catch (_) {}
|
||||
}
|
||||
this.sourceChains.delete(name);
|
||||
this.updateOperationalState();
|
||||
}
|
||||
|
||||
applySourceConditioning(immediate = false) {
|
||||
if (!this.am?.ctx) return;
|
||||
const now = this.am.ctx.currentTime;
|
||||
const tau = immediate ? 0.05 : 0.22; // ~1 second audible smoothing without sluggishness.
|
||||
for (const [name, c] of this.sourceChains) {
|
||||
const target = this.sourceMultiplier(name);
|
||||
try {
|
||||
c.comfortGain.gain.cancelScheduledValues(now);
|
||||
c.comfortGain.gain.setTargetAtTime(target, now, tau);
|
||||
} catch (_) {}
|
||||
}
|
||||
this.syncUI();
|
||||
}
|
||||
|
||||
releaseSourceConditioning(seconds = 1) {
|
||||
if (!this.am?.ctx) return;
|
||||
const now = this.am.ctx.currentTime,
|
||||
tau = Math.max(0.05, seconds / 4);
|
||||
for (const c of this.sourceChains.values()) {
|
||||
try {
|
||||
c.comfortGain.gain.cancelScheduledValues(now);
|
||||
c.comfortGain.gain.setTargetAtTime(1, now, tau);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (!this.enabled) return;
|
||||
this.ensureGraph();
|
||||
// Deterministic comfort attenuation is always active. HOLD only pauses watchdog learning.
|
||||
if (!this.holds.size && this.level > 0) this.analyzeGlobalWatchdog();
|
||||
this.updateOperationalState();
|
||||
}
|
||||
|
||||
updateOperationalState() {
|
||||
if (!this.enabled) {
|
||||
this.setState('OFF');
|
||||
return;
|
||||
}
|
||||
if (this.holds.size) {
|
||||
this.setState('HOLD');
|
||||
return;
|
||||
}
|
||||
this.setState('ACTIVE');
|
||||
if (this.ui.detail) {
|
||||
const l = this.sourceReductionPercent('lifeSupport');
|
||||
const h = this.sourceReductionPercent('hull');
|
||||
const w = this.sourceReductionPercent('worp');
|
||||
this.ui.detail.textContent = `Comfort suppression active — Life Support ${l}%, Hull ${h}%, Worp Core ${w}% reduction.`;
|
||||
}
|
||||
}
|
||||
|
||||
isProtected(freq) {
|
||||
const p = this.themeProfiles[this.themeId] || this.themeProfiles.starfleet;
|
||||
return p.protected.some(([lo, hi]) => freq >= lo && freq <= hi);
|
||||
}
|
||||
bucketFor(freq) {
|
||||
return Math.round(24 * Math.log2(freq / 100));
|
||||
}
|
||||
freqForBucket(bucket) {
|
||||
return 100 * Math.pow(2, bucket / 24);
|
||||
}
|
||||
|
||||
analyzeGlobalWatchdog() {
|
||||
if (this.holds.size || !this.analyser) return;
|
||||
const data = new Float32Array(this.analyser.frequencyBinCount);
|
||||
this.analyser.getFloatFrequencyData(data);
|
||||
const nyquist = this.am.ctx.sampleRate / 2,
|
||||
binHz = nyquist / data.length,
|
||||
candidates = [];
|
||||
for (
|
||||
let i = Math.max(3, Math.floor(220 / binHz));
|
||||
i < Math.min(data.length - 4, Math.ceil(8500 / binHz));
|
||||
i += 2
|
||||
) {
|
||||
const freq = i * binHz;
|
||||
if (this.isProtected(freq)) continue;
|
||||
const db = data[i];
|
||||
if (!Number.isFinite(db) || db < -68) continue;
|
||||
const local = (data[i - 3] + data[i - 2] + data[i + 2] + data[i + 3]) / 4;
|
||||
const prominence = db - local;
|
||||
const threshold = 10.5 - this.level * 1.5;
|
||||
if (prominence > threshold)
|
||||
candidates.push({ freq, db, prominence, bucket: this.bucketFor(freq) });
|
||||
}
|
||||
candidates.sort((a, b) => b.prominence + b.db * 0.03 - (a.prominence + a.db * 0.03));
|
||||
const seen = new Set();
|
||||
for (const c of candidates.slice(0, 4)) {
|
||||
if (seen.has(c.bucket)) continue;
|
||||
seen.add(c.bucket);
|
||||
const old = this.persistence.get(c.bucket) || { seconds: 0, prominence: 0 };
|
||||
old.seconds += this.intervalMs / 1000;
|
||||
old.prominence = old.prominence * 0.8 + c.prominence * 0.2;
|
||||
this.persistence.set(c.bucket, old);
|
||||
}
|
||||
for (const [b, v] of [...this.persistence])
|
||||
if (!seen.has(b)) {
|
||||
v.seconds = Math.max(0, v.seconds - (this.intervalMs / 1000) * 1.5);
|
||||
v.prominence *= 0.9;
|
||||
if (v.seconds <= 0.01) this.persistence.delete(b);
|
||||
}
|
||||
this.updateGlobalInterventions();
|
||||
}
|
||||
|
||||
updateGlobalInterventions() {
|
||||
const profile = this.themeProfiles[this.themeId] || this.themeProfiles.starfleet;
|
||||
const persistenceNeed = (95 - this.level * 20) / profile.bias;
|
||||
const eligible = [...this.persistence.entries()]
|
||||
.filter(([b, v]) => v.seconds >= persistenceNeed && !this.isProtected(this.freqForBucket(b)))
|
||||
.sort((a, b) => b[1].seconds + b[1].prominence * 2 - (a[1].seconds + a[1].prominence * 2))
|
||||
.slice(0, 2);
|
||||
if (eligible.length) {
|
||||
this.activeBuckets = eligible.map(([bucket, v]) => ({
|
||||
bucket,
|
||||
seconds: v.seconds,
|
||||
prominence: v.prominence
|
||||
}));
|
||||
this.applyActiveFilters();
|
||||
this.lastIntervention = performance.now();
|
||||
} else if (this.activeBuckets.length && performance.now() - this.lastIntervention > 12000) {
|
||||
this.activeBuckets = [];
|
||||
this.releaseAll(20);
|
||||
}
|
||||
}
|
||||
|
||||
targetDepth() {
|
||||
return -1.75 * (0.25 + this.level * 0.75);
|
||||
}
|
||||
|
||||
applyActiveFilters() {
|
||||
if (!this.filters.length) return;
|
||||
const now = this.am.ctx.currentTime,
|
||||
depth = this.targetDepth();
|
||||
this.filters.forEach((f, i) => {
|
||||
const a = this.activeBuckets[i];
|
||||
f.frequency.cancelScheduledValues(now);
|
||||
f.Q.cancelScheduledValues(now);
|
||||
f.gain.cancelScheduledValues(now);
|
||||
if (a) {
|
||||
const freq = this.freqForBucket(a.bucket);
|
||||
f.frequency.setTargetAtTime(freq, now, 4.5);
|
||||
f.Q.setTargetAtTime(5.5 + this.level * 2, now, 5.5);
|
||||
f.gain.setTargetAtTime(depth, now, 8);
|
||||
} else f.gain.setTargetAtTime(0, now, 8);
|
||||
});
|
||||
}
|
||||
|
||||
applyCurrentDepths() {
|
||||
if (this.activeBuckets.length) this.applyActiveFilters();
|
||||
else if (this.level <= 0) this.releaseAll(1.5);
|
||||
}
|
||||
releaseAll(seconds = 4) {
|
||||
if (!this.filters.length || !this.am.ctx) return;
|
||||
const now = this.am.ctx.currentTime;
|
||||
this.filters.forEach(f => {
|
||||
f.gain.cancelScheduledValues(now);
|
||||
f.gain.setTargetAtTime(0, now, Math.max(0.25, seconds / 4));
|
||||
});
|
||||
}
|
||||
setState(state) {
|
||||
this.state = state;
|
||||
this.syncUI();
|
||||
}
|
||||
|
||||
syncUI() {
|
||||
if (this.ui.slider) this.ui.slider.value = this.level;
|
||||
if (this.ui.value) this.ui.value.textContent = `${Math.round(this.level * 100)}%`;
|
||||
if (this.ui.button) {
|
||||
this.ui.button.textContent = this.enabled ? 'FNC ENABLED' : 'FNC BYPASS';
|
||||
this.ui.button.classList.toggle('enabled', this.enabled);
|
||||
this.ui.button.setAttribute('aria-pressed', String(this.enabled));
|
||||
}
|
||||
if (this.ui.state) {
|
||||
this.ui.state.textContent = this.enabled ? this.state : 'OFF';
|
||||
this.ui.state.className = `fnc-state ${(this.enabled ? this.state : 'OFF').toLowerCase()}`;
|
||||
}
|
||||
if (this.ui.detail && !this.enabled)
|
||||
this.ui.detail.textContent =
|
||||
'FNC bypassed. Hull Drone, Worp Core, and Life Support play at their configured channel levels.';
|
||||
else if (this.ui.detail && this.enabled && this.state === 'HOLD') {
|
||||
const l = this.sourceReductionPercent('lifeSupport'),
|
||||
h = this.sourceReductionPercent('hull'),
|
||||
w = this.sourceReductionPercent('worp');
|
||||
this.ui.detail.textContent = `Comfort suppression remains active during HOLD — Life Support ${l}%, Hull ${h}%, Worp Core ${w}%. Watchdog learning paused.`;
|
||||
} else if (this.ui.detail && this.enabled) {
|
||||
const l = this.sourceReductionPercent('lifeSupport'),
|
||||
h = this.sourceReductionPercent('hull'),
|
||||
w = this.sourceReductionPercent('worp');
|
||||
this.ui.detail.textContent = `Comfort suppression active — Life Support ${l}%, Hull ${h}%, Worp Core ${w}% reduction.`;
|
||||
}
|
||||
}
|
||||
}
|
||||
window.FuturisticNoiceCancellation = FuturisticNoiceCancellation;
|
||||
|
||||
|
||||
/**
|
||||
* Hull Drone & Environmental Sub-Bass Synthesizer
|
||||
@@ -278,7 +748,8 @@ class HullDroneSynth {
|
||||
// Connect Graph
|
||||
this.filterNode.connect(filterStage2);
|
||||
filterStage2.connect(this.gainNode);
|
||||
this.gainNode.connect(this.am.compressor);
|
||||
if (window.fncSystem) window.fncSystem.connectSource('hull', this.gainNode, () => this.params);
|
||||
else this.gainNode.connect(this.am.compressor);
|
||||
|
||||
// Start Sources
|
||||
this.subOsc1.start();
|
||||
@@ -306,6 +777,7 @@ class HullDroneSynth {
|
||||
this.stoppingTimeout = null;
|
||||
}
|
||||
if (this.nodes.length > 0) {
|
||||
if (window.fncSystem) window.fncSystem.detachSource('hull');
|
||||
try {
|
||||
if (this.subOsc1) this.subOsc1.stop();
|
||||
if (this.subOsc2) this.subOsc2.stop();
|
||||
@@ -457,7 +929,8 @@ class WarpCoreSynth {
|
||||
this.filterNode.connect(this.gainNode);
|
||||
}
|
||||
|
||||
this.gainNode.connect(this.am.compressor);
|
||||
if (window.fncSystem) window.fncSystem.connectSource('worp', this.gainNode, () => this.params);
|
||||
else this.gainNode.connect(this.am.compressor);
|
||||
|
||||
this.carrier.start();
|
||||
this.subOsc.start();
|
||||
@@ -611,6 +1084,7 @@ class WarpCoreSynth {
|
||||
this.pulseInterval = null;
|
||||
}
|
||||
if (this.nodes.length > 0) {
|
||||
if (window.fncSystem) window.fncSystem.detachSource('worp');
|
||||
try {
|
||||
if (this.carrier) this.carrier.stop();
|
||||
if (this.subOsc) this.subOsc.stop();
|
||||
@@ -724,7 +1198,8 @@ class LifeSupportSynth {
|
||||
delayRight.connect(merger, 0, 1); // Right delayed channel
|
||||
|
||||
merger.connect(this.gainNode);
|
||||
this.gainNode.connect(this.am.compressor);
|
||||
if (window.fncSystem) window.fncSystem.connectSource('lifeSupport', this.gainNode, () => this.params);
|
||||
else this.gainNode.connect(this.am.compressor);
|
||||
|
||||
this.noiseSource.start();
|
||||
airflowLfo.start();
|
||||
@@ -752,6 +1227,7 @@ class LifeSupportSynth {
|
||||
this.stoppingTimeout = null;
|
||||
}
|
||||
if (this.nodes.length > 0) {
|
||||
if (window.fncSystem) window.fncSystem.detachSource('lifeSupport');
|
||||
try {
|
||||
if (this.noiseSource) this.noiseSource.stop();
|
||||
} catch (e) {}
|
||||
@@ -811,7 +1287,7 @@ class TelemetrySynth {
|
||||
this.params = {
|
||||
volume: 0.4,
|
||||
density: 0.5, // How often background telemetry chirps occur (0 = off, 1 = busy bridge)
|
||||
era: 'tng', // 'tng', 'voyager', 'tos', 'ds9', 'nx', 'who', 'bioship', 'belter'
|
||||
era: 'tng', // 'tng', 'voyager', 'tos', 'ds9', 'nx', 'whataverse', 'bioship', 'belter'
|
||||
reverbMix: 0.25
|
||||
};
|
||||
|
||||
@@ -882,7 +1358,10 @@ class TelemetrySynth {
|
||||
// Calculate delay inversely proportional to density (2s to 12s)
|
||||
const baseDelay = 12000 * (1.05 - this.params.density);
|
||||
const jitter = Math.random() * 4000;
|
||||
const nextInterval = Math.max(800, baseDelay + jitter);
|
||||
let nextInterval = Math.max(800, baseDelay + jitter);
|
||||
if (['whataverse', 'tardix', 'what', 'who'].includes(this.params.era)) {
|
||||
nextInterval = Math.max(1200, nextInterval * 0.72);
|
||||
}
|
||||
|
||||
this.schedulerTimer = setTimeout(() => {
|
||||
this.playRandomTelemetrySound();
|
||||
@@ -894,8 +1373,11 @@ class TelemetrySynth {
|
||||
if (this.isMuted || this.params.volume <= 0.01 || !this.am.ctx) return;
|
||||
|
||||
switch (this.params.era) {
|
||||
case 'who':
|
||||
this.synthesizeWhoMechanicalTelemetry_();
|
||||
case 'whataverse':
|
||||
case 'tardix':
|
||||
case 'what':
|
||||
case 'who': // legacy preset compatibility
|
||||
this.synthesizeWhataverseMechanicalTelemetry_();
|
||||
break;
|
||||
case 'bioship':
|
||||
this.synthesizeBioshipOrganicTelemetry_();
|
||||
@@ -1298,80 +1780,107 @@ class TelemetrySynth {
|
||||
* (no electronic beeps). All transients scheduled on absolute ctx time so
|
||||
* the single scheduler timeout can never orphan nodes.
|
||||
*/
|
||||
synthesizeWhoMechanicalTelemetry_() {
|
||||
synthesizeWhataverseMechanicalTelemetry_() {
|
||||
this.init();
|
||||
const ctx = this.am.ctx;
|
||||
if (!ctx || !this.gainNode) return;
|
||||
|
||||
const now = ctx.currentTime;
|
||||
const strikeCount = 2 + Math.floor(Math.random() * 4); // 2-5 transients
|
||||
let cursor = Math.random() * 0.12;
|
||||
// Whataverse telemetry should read as an electromechanical console at room
|
||||
// distance, not as tiny UI clicks. Use a short phrase of clearly distinct
|
||||
// mechanical events with enough mid-band energy to survive the ambient bed.
|
||||
const eventCount = 3 + Math.floor(Math.random() * 4); // 3-6 events
|
||||
let cursor = 0.02 + Math.random() * 0.10;
|
||||
|
||||
// Toggle snap: dual-filtered short noise pulse (20-45 ms body)
|
||||
const toggleSnap = (t, peak) => {
|
||||
const buf = this.am.createNoiseBuffer('white', 0.06);
|
||||
const noise = ctx.createBufferSource();
|
||||
noise.buffer = buf;
|
||||
const bpLo = ctx.createBiquadFilter();
|
||||
bpLo.type = 'bandpass';
|
||||
bpLo.frequency.setValueAtTime(300 + Math.random() * 200, t);
|
||||
bpLo.Q.setValueAtTime(6, t);
|
||||
const bpHi = ctx.createBiquadFilter();
|
||||
bpHi.type = 'bandpass';
|
||||
bpHi.frequency.setValueAtTime(900 + Math.random() * 500, t);
|
||||
bpHi.Q.setValueAtTime(4, t);
|
||||
const noiseBurst = (t, type, freq, q, peak, dur) => {
|
||||
const buf = this.am.createNoiseBuffer(type, Math.max(0.12, dur + 0.04));
|
||||
const src = ctx.createBufferSource();
|
||||
src.buffer = buf;
|
||||
const bp = ctx.createBiquadFilter();
|
||||
bp.type = 'bandpass';
|
||||
bp.frequency.setValueAtTime(freq, t);
|
||||
bp.Q.setValueAtTime(q, t);
|
||||
const env = ctx.createGain();
|
||||
env.gain.setValueAtTime(0.001, t);
|
||||
env.gain.linearRampToValueAtTime(peak, t + 0.004);
|
||||
env.gain.exponentialRampToValueAtTime(0.001, t + 0.02 + Math.random() * 0.025);
|
||||
noise.connect(bpLo);
|
||||
bpLo.connect(env);
|
||||
noise.connect(bpHi);
|
||||
bpHi.connect(env);
|
||||
env.gain.exponentialRampToValueAtTime(0.001, t + dur);
|
||||
src.connect(bp);
|
||||
bp.connect(env);
|
||||
env.connect(this.gainNode);
|
||||
noise.start(t);
|
||||
noise.stop(t + 0.06);
|
||||
src.start(t);
|
||||
src.stop(t + dur + 0.03);
|
||||
};
|
||||
|
||||
// Spring lever snap: dull square click collapsing in pitch (60-120 Hz)
|
||||
// Dry toggle snap with a wood/plastic body and a small high-frequency edge.
|
||||
const toggleSnap = (t, peak) => {
|
||||
noiseBurst(t, 'pink', 720 + Math.random() * 520, 5.5, peak, 0.055 + Math.random() * 0.025);
|
||||
noiseBurst(t + 0.004, 'white', 1800 + Math.random() * 900, 4.0, peak * 0.34, 0.028);
|
||||
};
|
||||
|
||||
// Spring-loaded lever: audible mid-bass knock plus a metallic return tick.
|
||||
const springLever = (t, peak) => {
|
||||
const osc = ctx.createOscillator();
|
||||
osc.type = 'square';
|
||||
const f0 = 60 + Math.random() * 60;
|
||||
osc.type = 'triangle';
|
||||
const f0 = 150 + Math.random() * 90;
|
||||
osc.frequency.setValueAtTime(f0, t);
|
||||
osc.frequency.exponentialRampToValueAtTime(f0 * 0.5, t + 0.03);
|
||||
osc.frequency.exponentialRampToValueAtTime(70 + Math.random() * 25, t + 0.075);
|
||||
const env = ctx.createGain();
|
||||
env.gain.setValueAtTime(peak, t);
|
||||
env.gain.exponentialRampToValueAtTime(0.001, t + 0.03 + Math.random() * 0.02);
|
||||
env.gain.setValueAtTime(0.001, t);
|
||||
env.gain.linearRampToValueAtTime(peak, t + 0.004);
|
||||
env.gain.exponentialRampToValueAtTime(0.001, t + 0.11);
|
||||
osc.connect(env);
|
||||
env.connect(this.gainNode);
|
||||
osc.start(t);
|
||||
osc.stop(t + 0.055);
|
||||
osc.stop(t + 0.13);
|
||||
noiseBurst(t + 0.065, 'white', 1250 + Math.random() * 650, 7, peak * 0.42, 0.035);
|
||||
};
|
||||
|
||||
// Rotary selector clunk: short damped mechanical ping, never a clean beep
|
||||
const rotaryClunk = (t, peak) => {
|
||||
// Rotary selector: two or three obvious detents rather than one sine ping.
|
||||
const rotaryDetent = (t, peak) => {
|
||||
const detents = 2 + Math.floor(Math.random() * 2);
|
||||
for (let i = 0; i < detents; i++) {
|
||||
const dt = t + i * (0.045 + Math.random() * 0.018);
|
||||
noiseBurst(dt, 'pink', 520 + Math.random() * 420, 8, peak * (1 - i * 0.16), 0.042);
|
||||
const osc = ctx.createOscillator();
|
||||
osc.type = 'triangle';
|
||||
osc.frequency.setValueAtTime(330 + Math.random() * 160, dt);
|
||||
osc.frequency.exponentialRampToValueAtTime(190 + Math.random() * 60, dt + 0.055);
|
||||
const env = ctx.createGain();
|
||||
env.gain.setValueAtTime(peak * 0.55, dt);
|
||||
env.gain.exponentialRampToValueAtTime(0.001, dt + 0.07);
|
||||
osc.connect(env);
|
||||
env.connect(this.gainNode);
|
||||
osc.start(dt);
|
||||
osc.stop(dt + 0.08);
|
||||
}
|
||||
};
|
||||
|
||||
// Heavy relay/contactor gives the phrase a recognisable console-machine thunk.
|
||||
const relayThunk = (t, peak) => {
|
||||
const osc = ctx.createOscillator();
|
||||
osc.type = 'sine';
|
||||
const f0 = 180 + Math.random() * 260;
|
||||
osc.frequency.setValueAtTime(f0, t);
|
||||
osc.frequency.exponentialRampToValueAtTime(f0 * 0.7, t + 0.06);
|
||||
osc.type = 'square';
|
||||
osc.frequency.setValueAtTime(190 + Math.random() * 70, t);
|
||||
osc.frequency.exponentialRampToValueAtTime(65 + Math.random() * 20, t + 0.065);
|
||||
const env = ctx.createGain();
|
||||
env.gain.setValueAtTime(0.001, t);
|
||||
env.gain.linearRampToValueAtTime(peak, t + 0.003);
|
||||
env.gain.exponentialRampToValueAtTime(0.001, t + 0.06 + Math.random() * 0.08);
|
||||
env.gain.exponentialRampToValueAtTime(0.001, t + 0.105);
|
||||
osc.connect(env);
|
||||
env.connect(this.gainNode);
|
||||
osc.start(t);
|
||||
osc.stop(t + 0.16);
|
||||
osc.stop(t + 0.12);
|
||||
noiseBurst(t + 0.006, 'pink', 900 + Math.random() * 450, 5, peak * 0.48, 0.05);
|
||||
};
|
||||
|
||||
for (let i = 0; i < strikeCount; i++) {
|
||||
for (let i = 0; i < eventCount; i++) {
|
||||
const t = now + cursor;
|
||||
const r = Math.random();
|
||||
if (r < 0.5) toggleSnap(cursor, 0.08 + Math.random() * 0.08);
|
||||
else if (r < 0.8) springLever(cursor, 0.07 + Math.random() * 0.06);
|
||||
else rotaryClunk(cursor, 0.09 + Math.random() * 0.07);
|
||||
cursor += 0.04 + Math.random() * 0.14;
|
||||
const peak = 0.16 + Math.random() * 0.12;
|
||||
if (r < 0.34) toggleSnap(t, peak);
|
||||
else if (r < 0.60) springLever(t, peak);
|
||||
else if (r < 0.82) rotaryDetent(t, peak);
|
||||
else relayThunk(t, peak);
|
||||
cursor += 0.08 + Math.random() * 0.18;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6092,7 +6601,7 @@ class EngineTransitionSynth {
|
||||
case 'cardassian': this._startupCardassian(now, s); break;
|
||||
case 'tos': this._startupTOS(now, s); break;
|
||||
case 'nx': this._startupNX(now, s); break;
|
||||
case 'tardis': this._startupTARDIX(now, s); break;
|
||||
case 'tardix': this._startupTARDIX(now, s); break;
|
||||
case 'industrial': this._startupIndustrial(now, s); break;
|
||||
case 'bioship': this._startupBioship(now, s); break;
|
||||
case 'retrofuture': this._startupRetrofuture(now, s); break;
|
||||
@@ -6121,7 +6630,7 @@ class EngineTransitionSynth {
|
||||
case 'cardassian': this._shutdownCardassian(now, s); break;
|
||||
case 'tos': this._shutdownTOS(now, s); break;
|
||||
case 'nx': this._shutdownNX(now, s); break;
|
||||
case 'tardis': this._shutdownTARDIX(now, s); break;
|
||||
case 'tardix': this._shutdownTARDIX(now, s); break;
|
||||
case 'industrial': this._shutdownIndustrial(now, s); break;
|
||||
case 'bioship': this._shutdownBioship(now, s); break;
|
||||
case 'retrofuture': this._shutdownRetrofuture(now, s); break;
|
||||
@@ -6378,5 +6887,3 @@ class EngineTransitionSynth {
|
||||
}
|
||||
|
||||
window.EngineTransitionSynth = EngineTransitionSynth;
|
||||
|
||||
|
||||
|
||||
+1170
-1171
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+170
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* XZBT Control Bus & Local MIDI Controller
|
||||
*
|
||||
* 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 -- MIDI here, potentially other transports later -- talks to the bus rather
|
||||
* than reaching into app.js internals.
|
||||
*
|
||||
* XZBTMidiController maps Web MIDI CC / note messages onto those targets, with a
|
||||
* MIDI-learn flow and bindings persisted to localStorage. Web MIDI is a browser
|
||||
* API, not an external dependency: the offline single-file build is unaffected.
|
||||
*/
|
||||
|
||||
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;
|
||||
|
||||
class XZBTMidiController {
|
||||
constructor(bus) {
|
||||
this.bus = bus;
|
||||
this.access = null;
|
||||
this.bindings = {};
|
||||
this.learningTarget = null;
|
||||
this.ui = {};
|
||||
this.storageKey = 'xzbt-midi-bindings-v02';
|
||||
}
|
||||
bindUI() {
|
||||
const g = id => document.getElementById(id);
|
||||
this.ui = {
|
||||
status: g('midi-device-status'),
|
||||
enable: g('btn-midi-enable'),
|
||||
clear: g('btn-midi-clear'),
|
||||
target: g('midi-target-select'),
|
||||
learn: g('btn-midi-learn'),
|
||||
bindings: g('midi-bindings')
|
||||
};
|
||||
if (!this.ui.enable) return;
|
||||
this.load();
|
||||
this.refreshTargets();
|
||||
this.render();
|
||||
this.ui.enable.onclick = () => this.enable();
|
||||
this.ui.clear.onclick = () => {
|
||||
this.bindings = {};
|
||||
this.save();
|
||||
this.render();
|
||||
};
|
||||
this.ui.learn.onclick = () => {
|
||||
this.learningTarget = this.ui.target.value || null;
|
||||
this.ui.learn.classList.toggle('midi-learn-active', !!this.learningTarget);
|
||||
this.ui.learn.textContent = this.learningTarget
|
||||
? 'MOVE A MIDI CONTROL NOW...'
|
||||
: 'SELECT A TARGET FIRST';
|
||||
};
|
||||
}
|
||||
refreshTargets() {
|
||||
if (!this.ui.target) return;
|
||||
this.ui.target.innerHTML = '';
|
||||
this.bus.list().forEach(t => {
|
||||
const o = document.createElement('option');
|
||||
o.value = t.id;
|
||||
o.textContent = t.label || t.id;
|
||||
this.ui.target.appendChild(o);
|
||||
});
|
||||
}
|
||||
async enable() {
|
||||
if (!navigator.requestMIDIAccess) {
|
||||
this.ui.status.textContent = 'WEB MIDI IS NOT AVAILABLE IN THIS BROWSER.';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.access = await navigator.requestMIDIAccess({ sysex: false });
|
||||
this.attach();
|
||||
this.access.onstatechange = () => this.attach();
|
||||
this.ui.status.textContent = `MIDI ONLINE - ${[...this.access.inputs.values()].map(x => x.name).join(', ') || 'NO INPUT DEVICES'}`;
|
||||
} catch (e) {
|
||||
this.ui.status.textContent = `MIDI ERROR - ${e.message || e}`;
|
||||
}
|
||||
}
|
||||
attach() {
|
||||
if (!this.access) return;
|
||||
for (const input of this.access.inputs.values())
|
||||
input.onmidimessage = e => this.onMessage(e, input);
|
||||
}
|
||||
keyFor(input, status, d1) {
|
||||
const cmd = status & 0xf0,
|
||||
ch = (status & 0x0f) + 1;
|
||||
return `${input.id}|${cmd}|${ch}|${d1}`;
|
||||
}
|
||||
onMessage(e, input) {
|
||||
const [status, d1, d2] = e.data,
|
||||
cmd = status & 0xf0;
|
||||
if (cmd !== 0xb0 && cmd !== 0x90 && cmd !== 0x80) return;
|
||||
const key = this.keyFor(input, status, d1);
|
||||
if (this.learningTarget) {
|
||||
this.bindings[key] = {
|
||||
target: this.learningTarget,
|
||||
inputName: input.name || 'MIDI',
|
||||
cmd,
|
||||
channel: (status & 15) + 1,
|
||||
data1: d1
|
||||
};
|
||||
this.learningTarget = null;
|
||||
this.ui.learn.classList.remove('midi-learn-active');
|
||||
this.ui.learn.textContent = 'MIDI LEARN: SELECT TARGET, THEN MOVE CONTROL';
|
||||
this.save();
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
const b = this.bindings[key];
|
||||
if (!b) return;
|
||||
const t = this.bus.targets.get(b.target);
|
||||
if (!t) return;
|
||||
if (t.type === 'action') {
|
||||
if (cmd === 0x90 && d2 > 0) this.bus.trigger(b.target, 'midi');
|
||||
} else {
|
||||
const norm = d2 / 127;
|
||||
this.bus.set(b.target, t.min + norm * (t.max - t.min), 'midi');
|
||||
}
|
||||
}
|
||||
save() {
|
||||
try {
|
||||
localStorage.setItem(this.storageKey, JSON.stringify(this.bindings));
|
||||
} catch (_) {}
|
||||
}
|
||||
load() {
|
||||
try {
|
||||
this.bindings = JSON.parse(localStorage.getItem(this.storageKey) || '{}') || {};
|
||||
} catch (_) {
|
||||
this.bindings = {};
|
||||
}
|
||||
}
|
||||
render() {
|
||||
if (!this.ui.bindings) return;
|
||||
const rows = Object.values(this.bindings).map(
|
||||
b =>
|
||||
`${b.inputName} CH${b.channel} ${b.cmd === 0xb0 ? 'CC' : 'NOTE'} ${b.data1} -> ${b.target}`
|
||||
);
|
||||
this.ui.bindings.textContent = rows.join('\n') || 'NO MIDI BINDINGS';
|
||||
}
|
||||
}
|
||||
window.XZBTMidiController = XZBTMidiController;
|
||||
+270
-270
@@ -1,270 +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 === 'whoniverse') {
|
||||
// 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['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.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>
|
||||
`;
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 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>
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
+3589
-2783
File diff suppressed because it is too large
Load Diff
+112
-76
@@ -1,77 +1,113 @@
|
||||
<#
|
||||
.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
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Packages the modular SciFi-XZBT project into a single, standalone, offline HTML file.
|
||||
.EXAMPLE
|
||||
.\package.ps1
|
||||
.EXAMPLE
|
||||
.\package.ps1 -Force
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$OutputFile = "",
|
||||
[switch]$Force
|
||||
)
|
||||
|
||||
$rootDir = (Resolve-Path "$PSScriptRoot\..").Path
|
||||
$indexFile = Join-Path $rootDir "index.html"
|
||||
|
||||
if (-not (Test-Path $indexFile)) {
|
||||
Write-Error "index.html not found in $rootDir"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Resolve the current commit hash so the output filename is tied to the
|
||||
# exact source snapshot it was built from, e.g. SciFiAmbientDisplay_V977d348.html
|
||||
$commitHash = $null
|
||||
try {
|
||||
$commitHash = (git -C $rootDir rev-parse --short HEAD 2>$null)
|
||||
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($commitHash)) {
|
||||
$commitHash = $null
|
||||
} else {
|
||||
$commitHash = $commitHash.Trim()
|
||||
}
|
||||
} catch {
|
||||
$commitHash = $null
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($commitHash)) {
|
||||
Write-Error "Unable to resolve the current git commit hash (is git installed and is $rootDir a git repository?). Aborting so the build isn't silently mislabeled."
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($OutputFile)) {
|
||||
$OutputFile = Join-Path $rootDir "dist\SciFiAmbientDisplay_V$commitHash.html"
|
||||
}
|
||||
|
||||
if (Test-Path $OutputFile) {
|
||||
Write-Error "Output file already exists: $OutputFile"
|
||||
if ($Force) {
|
||||
Write-Warning "-Force specified; overwriting existing file."
|
||||
} else {
|
||||
$response = Read-Host "This build has already been generated. Replace it? (y/N)"
|
||||
if ($response -notmatch '^(y|yes)$') {
|
||||
Write-Host "Aborted. No changes made." -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Packaging SciFi Ambient Display from $rootDir..." -ForegroundColor Cyan
|
||||
Write-Host " -> Commit: $commitHash" -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