Checkpoint ambient display, generative experience, and MIDI work
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
# SciFi Ambient Display
|
# 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
|
## 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.
|
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.
|
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
|
## 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.
|
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
|
## 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% |
|
| 0 | Set volume to 100% |
|
||||||
| W | Trigger the universe's primary event (e.g. warp jump) |
|
| W | Trigger the universe's primary event (e.g. warp jump) |
|
||||||
| R | Trigger an alert |
|
| R | Trigger an alert |
|
||||||
| F | Toggle the observation viewport frame (in Observation Mode) |
|
| F | Toggle the viewport frame (in the Watch Experience) |
|
||||||
| P | Toggle support pillars (in Observation Mode) |
|
| P | Toggle support pillars (in the Watch Experience) |
|
||||||
| Esc | Exit Observation Mode / stop active alerts |
|
| Esc | Exit the Watch Experience / stop active alerts |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -77,12 +105,15 @@ SciFi-XZBT/
|
|||||||
├── js/
|
├── js/
|
||||||
│ ├── audio.js # Web Audio synthesis, oscillators, sound engines
|
│ ├── audio.js # Web Audio synthesis, oscillators, sound engines
|
||||||
│ ├── config.js # Vessel presets, universe registry & palettes
|
│ ├── config.js # Vessel presets, universe registry & palettes
|
||||||
|
│ ├── core-animations.js # Settings-panel core animations
|
||||||
│ ├── visualizer.js # Audio spectrum & Worp Core canvas rendering
|
│ ├── visualizer.js # Audio spectrum & Worp Core canvas rendering
|
||||||
│ ├── observation-bezels.js # Procedural SVG viewport bezels
|
│ ├── 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
|
│ └── app.js # DOM bindings, hotkeys, UI controllers & main loop
|
||||||
├── dist/
|
├── dist/
|
||||||
│ └── SciFiAmbientDisplay_v4.html # Standalone, single-file offline build
|
│ └── SciFiAmbientDisplay_V<commit>.html # Standalone, single-file offline build
|
||||||
└── tools/
|
└── tools/
|
||||||
├── package.ps1 # 1-click script to build the standalone single-file HTML
|
├── package.ps1 # 1-click script to build the standalone single-file HTML
|
||||||
└── extract.ps1 # Original extraction script for reference
|
└── extract.ps1 # Original extraction script for reference
|
||||||
@@ -93,8 +124,11 @@ SciFi-XZBT/
|
|||||||
- Edit the specific CSS or JS files directly.
|
- Edit the specific CSS or JS files directly.
|
||||||
- Audio/synthesizer logic lives in `js/audio.js`.
|
- Audio/synthesizer logic lives in `js/audio.js`.
|
||||||
- Viewscreen bezels and framing are in `js/observation-bezels.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`.
|
- 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
|
### Repackaging to a single file
|
||||||
|
|
||||||
@@ -102,6 +136,6 @@ SciFi-XZBT/
|
|||||||
powershell -ExecutionPolicy Bypass -File .\tools\package.ps1
|
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>
|
</details>
|
||||||
|
|||||||
@@ -14,12 +14,13 @@ package manager, no server, and no test suite.**
|
|||||||
|
|
||||||
## Architecture
|
## 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
|
css/style.css
|
||||||
js/audio.js -> js/config.js -> js/visualizer.js
|
js/audio.js -> js/config.js -> js/core-animations.js -> js/visualizer.js
|
||||||
js/observation-bezels.js -> js/observation-engine.js -> js/app.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
|
Order matters: every file defines plain classes/objects and publishes them on
|
||||||
@@ -28,16 +29,19 @@ tags without checking the dependencies.
|
|||||||
|
|
||||||
| File | Owns | ~Lines |
|
| File | Owns | ~Lines |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `index.html` | DOM skeleton, script/link tags | 370 |
|
| `index.html` | DOM skeleton, script/link tags | 460 |
|
||||||
| `css/style.css` | LCARD themes, CRT effects, palettes | 2,300 |
|
| `css/style.css` | LCARD themes, CRT effects, palettes | 2,430 |
|
||||||
| `js/audio.js` | `AudioManager` + all synth classes (hull, warp, life support, telemetry, alert, Whataverse, expanded sci-fi) | 2,250 |
|
| `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,040 |
|
| `js/config.js` | `StarshipPresets`, `UniverseRegistry` — data only, no behavior | 1,170 |
|
||||||
| `js/visualizer.js` | `StarshipVisualizer` — spectrum + worp core canvas | 640 |
|
| `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-bezels.js` | `ObservationBezels` — procedural SVG viewport frames per era | 270 |
|
||||||
| `js/observation-engine.js` | `ObservationEngine` — celestial simulation and canvas rendering | 2,780 |
|
| `js/observation-engine.js` | `ObservationEngine` — celestial simulation, canvas rendering, per-universe waveform | 3,590 |
|
||||||
| `js/app.js` | DOM bindings, hotkeys, UI controllers, main loop | 3,960 |
|
| `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 | — |
|
| `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
|
## Hard constraints
|
||||||
|
|
||||||
@@ -45,6 +49,32 @@ tags without checking the dependencies.
|
|||||||
audio files. The packaged build must work when double-clicked while offline.
|
audio files. The packaged build must work when double-clicked while offline.
|
||||||
- **No ES modules.** No `import` / `export`, no `type="module"`. Scripts are
|
- **No ES modules.** No `import` / `export`, no `type="module"`. Scripts are
|
||||||
classic globals.
|
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;`,
|
- **Publish globals explicitly.** End each file with `window.Thing = Thing;`,
|
||||||
matching the existing pattern.
|
matching the existing pattern.
|
||||||
- **All audio is synthesized**, not sampled. New sounds are built from
|
- **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`
|
- New room or ship preset, universe, palette → `js/config.js`
|
||||||
- Sound generation, oscillators, envelopes, filters → `js/audio.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`
|
- Spectrum bars, warp-core animation → `js/visualizer.js`
|
||||||
- Viewscreen frame / bezel shapes per era → `js/observation-bezels.js`
|
- Viewscreen frame / bezel shapes per era → `js/observation-bezels.js`
|
||||||
- Planets, stars, drift, observation-mode canvas → `js/observation-engine.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:
|
There are no automated tests. Verification is manual:
|
||||||
|
|
||||||
1. Open `index.html` in a browser; check the console is clean.
|
1. Open `index.html` in a browser; check the console is clean.
|
||||||
2. Exercise the affected area — play a preset, switch era/universe, open
|
2. Exercise the affected area — play a preset, switch era/universe, enter the
|
||||||
observation mode.
|
Watch Experience, toggle FNC.
|
||||||
3. Re-run `package.ps1` and open `dist/SciFiAmbientDisplay_v4.html` **with the
|
3. Re-run `package.ps1` and open the generated `dist/SciFiAmbientDisplay_V<commit>.html`
|
||||||
network disabled** to confirm it is genuinely self-contained.
|
**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
|
Browser autoplay policy blocks audio until a user gesture; audio starting only
|
||||||
after a click is expected behavior, not a bug.
|
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
|
## Conventions and cautions
|
||||||
|
|
||||||
- 2-space indentation; single quotes in JS; existing brace and spacing style.
|
- 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
|
- `js/app.js`, `js/audio.js` and `js/observation-engine.js` are large. Make
|
||||||
edits** — do not rewrite, reorder, or reformat whole files.
|
**surgical, scoped edits** — do not rewrite, reorder, or reformat whole files.
|
||||||
- Do not reformat or lint files wholesale; diffs should stay minimal and
|
- Do not reformat or lint files wholesale; diffs should stay minimal and
|
||||||
reviewable.
|
reviewable.
|
||||||
- Never edit anything in `dist/` — regenerate it instead.
|
- Never edit anything in `dist/` — regenerate it instead.
|
||||||
- Preserve the offline, single-file-deliverable property in every change.
|
- 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.
|
|
||||||
+95
-36
@@ -614,6 +614,25 @@ body {
|
|||||||
box-shadow: 0 0 4px #ff3333;
|
box-shadow: 0 0 4px #ff3333;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Futuristic Noice Cancellation (FNC) */
|
||||||
|
.fnc-console{
|
||||||
|
margin-top:2px;padding:10px 12px;border:1px solid rgba(0,230,255,.26);
|
||||||
|
border-left:5px solid var(--tertiary-accent);border-radius:6px;
|
||||||
|
background:rgba(0,230,255,.035);display:flex;flex-direction:column;gap:8px;
|
||||||
|
}
|
||||||
|
.fnc-top{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}
|
||||||
|
.fnc-title-group{display:flex;align-items:baseline;gap:9px;flex-wrap:wrap}
|
||||||
|
.fnc-title{font-family:var(--lcars-font);font-weight:700;color:var(--tertiary-accent);letter-spacing:.08em}
|
||||||
|
.fnc-state{font-family:var(--mono-font);font-size:.72rem;color:#9fb4c9;letter-spacing:.08em}
|
||||||
|
.fnc-state.active{color:#00ff88;text-shadow:0 0 7px rgba(0,255,136,.45)}
|
||||||
|
.fnc-state.hold{color:#ffcc00}.fnc-state.recovering{color:#7dd3fc}.fnc-state.off{color:#778899}
|
||||||
|
.btn-fnc{background:#152333;color:var(--tertiary-accent);border:1px solid var(--tertiary-accent)}
|
||||||
|
.btn-fnc.enabled{background:var(--tertiary-accent);color:#000;box-shadow:0 0 10px color-mix(in srgb,var(--tertiary-accent) 55%,transparent)}
|
||||||
|
.fnc-level-row{display:grid;grid-template-columns:auto 1fr auto;gap:10px;align-items:center}
|
||||||
|
.fnc-level-label,.fnc-level-value{font-family:var(--mono-font);font-size:.72rem;color:#c7d5e5;white-space:nowrap}
|
||||||
|
.fnc-level-value{min-width:42px;text-align:right;color:var(--tertiary-accent)}
|
||||||
|
.fnc-detail{font-family:var(--mono-font);font-size:.61rem;line-height:1.35;color:#71869a;min-height:1.35em}
|
||||||
|
|
||||||
/* Multi-Channel Synthesizer Mixer */
|
/* Multi-Channel Synthesizer Mixer */
|
||||||
.mixer-grid {
|
.mixer-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -879,18 +898,18 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
WHATAVERSE (TARDIX CONSOLE) THEME
|
WHATAVERSE (TARDIX CONSOLE) THEME
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
|
|
||||||
body.theme-whoniverse-tardis {
|
body.theme-whataverse-tardix {
|
||||||
--tardis-blue: #003b6f;
|
--tardix-blue: #003b6f;
|
||||||
--tardis-deep: #001f3f;
|
--tardix-deep: #001f3f;
|
||||||
--tardis-cyan: #00e5ff;
|
--tardix-cyan: #00e5ff;
|
||||||
--tardis-teal: #008080;
|
--tardix-teal: #008080;
|
||||||
--tardis-amber: #d4af37;
|
--tardix-amber: #d4af37;
|
||||||
--tardis-brass: #b8860b;
|
--tardix-brass: #b8860b;
|
||||||
--tardis-roundel: #e0f2fe;
|
--tardix-roundel: #e0f2fe;
|
||||||
|
|
||||||
--primary-accent: var(--tardis-cyan);
|
--primary-accent: var(--tardix-cyan);
|
||||||
--secondary-accent: var(--tardis-blue);
|
--secondary-accent: var(--tardix-blue);
|
||||||
--tertiary-accent: var(--tardis-amber);
|
--tertiary-accent: var(--tardix-amber);
|
||||||
--warning-accent: #cc0000;
|
--warning-accent: #cc0000;
|
||||||
|
|
||||||
--lcars-bg: #030712;
|
--lcars-bg: #030712;
|
||||||
@@ -903,75 +922,75 @@ body.theme-whoniverse-tardis {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Header Styling for TARDIX Police Box */
|
/* Header Styling for TARDIX Police Box */
|
||||||
body.theme-whoniverse-tardis .lcars-elbow-left {
|
body.theme-whataverse-tardix .lcars-elbow-left {
|
||||||
background: linear-gradient(135deg, #004a8b, #002244);
|
background: linear-gradient(135deg, #004a8b, #002244);
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
border: 1px solid var(--tardis-cyan);
|
border: 1px solid var(--tardix-cyan);
|
||||||
border-radius: 4px 4px 0 0;
|
border-radius: 4px 4px 0 0;
|
||||||
box-shadow: 0 0 12px rgba(0, 229, 255, 0.3);
|
box-shadow: 0 0 12px rgba(0, 229, 255, 0.3);
|
||||||
letter-spacing: 0.15em;
|
letter-spacing: 0.15em;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.theme-whoniverse-tardis .lcars-top-bar {
|
body.theme-whataverse-tardix .lcars-top-bar {
|
||||||
background: #001f3f;
|
background: #001f3f;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
border: 1px solid rgba(0, 229, 255, 0.4);
|
border: 1px solid rgba(0, 229, 255, 0.4);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.theme-whoniverse-tardis .header-vol-val {
|
body.theme-whataverse-tardix .header-vol-val {
|
||||||
color: var(--tardis-cyan);
|
color: var(--tardix-cyan);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.theme-whoniverse-tardis .lcars-pill-end {
|
body.theme-whataverse-tardix .lcars-pill-end {
|
||||||
background-color: var(--tardis-amber);
|
background-color: var(--tardix-amber);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TARDIX Roundel Buttons & Cards */
|
/* TARDIX Roundel Buttons & Cards */
|
||||||
body.theme-whoniverse-tardis .sidebar-heading {
|
body.theme-whataverse-tardix .sidebar-heading {
|
||||||
background: linear-gradient(90deg, #003b6f, #001f3f);
|
background: linear-gradient(90deg, #003b6f, #001f3f);
|
||||||
color: var(--tardis-cyan);
|
color: var(--tardix-cyan);
|
||||||
border-left: 4px solid var(--tardis-cyan);
|
border-left: 4px solid var(--tardix-cyan);
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.theme-whoniverse-tardis .preset-btn {
|
body.theme-whataverse-tardix .preset-btn {
|
||||||
background: rgba(0, 59, 111, 0.2);
|
background: rgba(0, 59, 111, 0.2);
|
||||||
border: 1px solid rgba(0, 229, 255, 0.2);
|
border: 1px solid rgba(0, 229, 255, 0.2);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: #e2e8f0;
|
color: #e2e8f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.theme-whoniverse-tardis .preset-btn:hover {
|
body.theme-whataverse-tardix .preset-btn:hover {
|
||||||
background: rgba(0, 229, 255, 0.15);
|
background: rgba(0, 229, 255, 0.15);
|
||||||
border-color: var(--tardis-cyan);
|
border-color: var(--tardix-cyan);
|
||||||
box-shadow: 0 0 8px rgba(0, 229, 255, 0.3);
|
box-shadow: 0 0 8px rgba(0, 229, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.theme-whoniverse-tardis .preset-btn.active {
|
body.theme-whataverse-tardix .preset-btn.active {
|
||||||
background: linear-gradient(135deg, #003b6f, #002244);
|
background: linear-gradient(135deg, #003b6f, #002244);
|
||||||
border: 1px solid var(--tardis-cyan);
|
border: 1px solid var(--tardix-cyan);
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
box-shadow: 0 0 15px rgba(0, 229, 255, 0.5);
|
box-shadow: 0 0 15px rgba(0, 229, 255, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Master Play Button for TARDIX */
|
/* Master Play Button for TARDIX */
|
||||||
body.theme-whoniverse-tardis .btn-lcars-large {
|
body.theme-whataverse-tardix .btn-lcars-large {
|
||||||
background: linear-gradient(135deg, #00e5ff, #0088cc);
|
background: linear-gradient(135deg, #00e5ff, #0088cc);
|
||||||
color: #000000;
|
color: #000000;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 0 16px rgba(0, 229, 255, 0.4);
|
box-shadow: 0 0 16px rgba(0, 229, 255, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.theme-whoniverse-tardis .btn-lcars-large.playing {
|
body.theme-whataverse-tardix .btn-lcars-large.playing {
|
||||||
background: linear-gradient(135deg, #d4af37, #996515);
|
background: linear-gradient(135deg, #d4af37, #996515);
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
box-shadow: 0 0 20px rgba(212, 175, 55, 0.6);
|
box-shadow: 0 0 20px rgba(212, 175, 55, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Roundel Keypad Buttons */
|
/* Roundel Keypad Buttons */
|
||||||
body.theme-whoniverse-tardis .btn-chirp {
|
body.theme-whataverse-tardix .btn-chirp {
|
||||||
background: radial-gradient(circle, rgba(0, 229, 255, 0.1) 0%, rgba(0, 31, 63, 0.4) 100%);
|
background: radial-gradient(circle, rgba(0, 229, 255, 0.1) 0%, rgba(0, 31, 63, 0.4) 100%);
|
||||||
border: 1px solid rgba(0, 229, 255, 0.4);
|
border: 1px solid rgba(0, 229, 255, 0.4);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
@@ -979,14 +998,14 @@ body.theme-whoniverse-tardis .btn-chirp {
|
|||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.theme-whoniverse-tardis .btn-chirp:hover {
|
body.theme-whataverse-tardix .btn-chirp:hover {
|
||||||
background: var(--tardis-cyan);
|
background: var(--tardix-cyan);
|
||||||
color: #000000;
|
color: #000000;
|
||||||
box-shadow: 0 0 12px var(--tardis-cyan);
|
box-shadow: 0 0 12px var(--tardix-cyan);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cloister Chime Active Siren */
|
/* Cloister Chime Active Siren */
|
||||||
body.theme-whoniverse-tardis .btn-lcars-pill.active-alert {
|
body.theme-whataverse-tardix .btn-lcars-pill.active-alert {
|
||||||
background-color: #ff3300 !important;
|
background-color: #ff3300 !important;
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
animation: cloister-pulse 1.2s infinite alternate;
|
animation: cloister-pulse 1.2s infinite alternate;
|
||||||
@@ -998,9 +1017,9 @@ body.theme-whoniverse-tardis .btn-lcars-pill.active-alert {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* TARDIX Footer */
|
/* TARDIX Footer */
|
||||||
body.theme-whoniverse-tardis .lcars-footer {
|
body.theme-whataverse-tardix .lcars-footer {
|
||||||
background: linear-gradient(90deg, #001f3f, #003b6f);
|
background: linear-gradient(90deg, #001f3f, #003b6f);
|
||||||
color: var(--tardis-cyan);
|
color: var(--tardix-cyan);
|
||||||
border-top: 1px solid rgba(0, 229, 255, 0.3);
|
border-top: 1px solid rgba(0, 229, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1845,7 +1864,7 @@ body.theme-comedy .preset-btn.active {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* OBSERVATION-specific theme tinting */
|
/* OBSERVATION-specific theme tinting */
|
||||||
body.theme-whoniverse-tardis .observation-overlay { background: #020817; }
|
body.theme-whataverse-tardix .observation-overlay { background: #020817; }
|
||||||
body.theme-industrial .observation-overlay { background: #070706; }
|
body.theme-industrial .observation-overlay { background: #070706; }
|
||||||
body.theme-bioships .observation-overlay { background: #010b07; }
|
body.theme-bioships .observation-overlay { background: #010b07; }
|
||||||
body.theme-retrofuture .observation-overlay { background: #010903; }
|
body.theme-retrofuture .observation-overlay { background: #010903; }
|
||||||
@@ -2371,3 +2390,43 @@ body.theme-comedy .observation-overlay { background: #07061a; }
|
|||||||
animation: obs-scene-flicker 10s steps(1,end) infinite;
|
animation: obs-scene-flicker 10s steps(1,end) infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* === XZBT GENERATIVE EXPERIENCE / WEBLLM + KOKORO + MIDI + DETERMINISTIC FNC COMFORT FILTER v3.7 === */
|
||||||
|
.ai-experience-card .ai-status-line{display:flex;justify-content:space-between;gap:8px;align-items:center;margin-bottom:8px;font-family:var(--mono-font);font-size:.72rem;letter-spacing:.06em}
|
||||||
|
.ai-status-badge{padding:3px 7px;border:1px solid var(--primary-accent);border-radius:3px;color:var(--primary-accent);white-space:nowrap}
|
||||||
|
.ai-status-badge.ready{color:#00ff88;border-color:#00ff88}.ai-status-badge.busy{color:#ffcc00;border-color:#ffcc00}.ai-status-badge.error{color:#ff6666;border-color:#ff6666}
|
||||||
|
.ai-control-grid{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin:8px 0}.ai-control-grid .span-2{grid-column:span 2}
|
||||||
|
.ai-experience-card select,.ai-experience-card input[type="range"]{width:100%;box-sizing:border-box}.ai-experience-card select{background:#05070a;color:var(--tertiary-accent);border:1px solid var(--secondary-accent);padding:6px;font-family:var(--mono-font);font-size:.72rem}
|
||||||
|
.ai-toggle-row{display:flex;align-items:center;gap:7px;font-family:var(--mono-font);font-size:.72rem;min-height:28px}
|
||||||
|
.ai-experience-output{min-height:54px;max-height:116px;overflow-y:auto;margin-top:8px;padding:8px;border-left:3px solid var(--primary-accent);background:rgba(0,0,0,.38);color:#dce7f7;font-family:var(--mono-font);font-size:.72rem;line-height:1.45}
|
||||||
|
.ai-progress{margin-top:6px;font-family:var(--mono-font);font-size:.66rem;color:var(--tertiary-accent);min-height:1.1em}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* === XZBT v3.7: Watch Experience / TV / MIDI / Neural Speech / Deterministic FNC Comfort Filter === */
|
||||||
|
.watch-experience-wrap{display:flex;flex-direction:column;gap:3px;min-width:190px}
|
||||||
|
.watch-experience-wrap .btn-observation{font-weight:700;letter-spacing:.08em}
|
||||||
|
.watch-experience-help{font-family:var(--mono-font);font-size:.58rem;line-height:1.2;color:var(--tertiary-accent);opacity:.86;max-width:230px}
|
||||||
|
.ai-fixed-model{padding:7px;border:1px solid rgba(255,255,255,.18);background:rgba(0,0,0,.3);font-family:var(--mono-font);font-size:.68rem;line-height:1.45}
|
||||||
|
.ai-fixed-model b{color:var(--primary-accent)}
|
||||||
|
.ai-engine-row{display:grid;grid-template-columns:1fr 1fr;gap:6px}
|
||||||
|
.ai-engine-chip{padding:6px;border:1px solid rgba(255,255,255,.16);font-family:var(--mono-font);font-size:.62rem}
|
||||||
|
.ai-engine-chip span{float:right;color:var(--tertiary-accent)}
|
||||||
|
.midi-card .midi-device-line{font-family:var(--mono-font);font-size:.67rem;line-height:1.35;margin:5px 0;color:var(--tertiary-accent)}
|
||||||
|
.midi-bindings{max-height:150px;overflow:auto;padding:6px;background:rgba(0,0,0,.28);font-family:var(--mono-font);font-size:.61rem;line-height:1.45;white-space:pre-wrap}
|
||||||
|
.midi-learn-active{outline:2px solid #ffcc00!important;box-shadow:0 0 12px rgba(255,204,0,.25)}
|
||||||
|
.tv-mode-card{font-family:var(--mono-font);font-size:.65rem}
|
||||||
|
.tv-mode-card select{width:100%;background:#05070a;color:var(--tertiary-accent);border:1px solid var(--secondary-accent);padding:6px;font-family:var(--mono-font)}
|
||||||
|
body.xzbt-tv-letterbox{background:#000}
|
||||||
|
/* Canonical 16:9 presentation viewport. Children continue using percentage/absolute geometry. */
|
||||||
|
body.xzbt-tv-aspect .observation-overlay{
|
||||||
|
width:min(100vw,calc(100vh * 16 / 9));
|
||||||
|
height:min(100vh,calc(100vw * 9 / 16));
|
||||||
|
top:50%;left:50%;right:auto;bottom:auto;
|
||||||
|
transform:translate(-50%,-50%);
|
||||||
|
box-shadow:0 0 0 100vmax #000;
|
||||||
|
}
|
||||||
|
body.xzbt-tv-aspect .observation-overlay:not(.active){display:none}
|
||||||
|
@media (max-width:700px){
|
||||||
|
.watch-experience-wrap{min-width:150px}.watch-experience-help{font-size:.54rem}
|
||||||
|
}
|
||||||
|
|||||||
+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>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="theme-lcars-tng">
|
<body class="theme-lcars-tng">
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
<button class="btn-header-mute" id="btn-header-mute" title="Toggle Mute (M)">MUTE</button>
|
<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>
|
<span class="header-vol-val" id="header-vol-val">75%</span>
|
||||||
</div>
|
</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>
|
||||||
<div class="lcars-pill-end"></div>
|
<div class="lcars-pill-end"></div>
|
||||||
</header>
|
</header>
|
||||||
@@ -84,9 +84,10 @@
|
|||||||
<div class="pill-button-group" id="universe-events-container">
|
<div class="pill-button-group" id="universe-events-container">
|
||||||
<!-- Dynamically populated per universe -->
|
<!-- Dynamically populated per universe -->
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-lcars-pill btn-observation" id="btn-observation" title="Enter passive full-screen observation display">
|
<div class="watch-experience-wrap">
|
||||||
OBSERVATION
|
<button class="btn-lcars-pill btn-observation" id="btn-observation" title="Start the full-screen procedural visual and sound experience">WATCH EXPERIENCE</button>
|
||||||
</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)">
|
<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>
|
<span class="observation-hud-label">VIEWPORT</span>
|
||||||
<input type="checkbox" id="toggle-observation-viewport" checked aria-label="Toggle Observation Viewport Frame">
|
<input type="checkbox" id="toggle-observation-viewport" checked aria-label="Toggle Observation Viewport Frame">
|
||||||
@@ -148,6 +149,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<button class="vol-adj-btn" id="btn-vol-up" title="Volume Up (+5% or Up Arrow)">+</button>
|
<button class="vol-adj-btn" id="btn-vol-up" title="Volume Up (+5% or Up Arrow)">+</button>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -261,6 +279,79 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Sleep Timer & Auto-Fadeout Card -->
|
||||||
<div class="panel-card">
|
<div class="panel-card">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
@@ -291,7 +382,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<!-- OBSERVATION: passive casting / display mode. Click background to return. -->
|
<!-- 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) -->
|
<!-- Layer 0 & 1: 60fps Deep Celestial Canvas (Parallax 3D Stars, Warp Streaks, Nebulae, Planets, Encounters) -->
|
||||||
<canvas id="observation-canvas" class="observation-canvas"></canvas>
|
<canvas id="observation-canvas" class="observation-canvas"></canvas>
|
||||||
|
|
||||||
@@ -363,6 +454,8 @@
|
|||||||
<script src="js/visualizer.js"></script>
|
<script src="js/visualizer.js"></script>
|
||||||
<script src="js/observation-bezels.js"></script>
|
<script src="js/observation-bezels.js"></script>
|
||||||
<script src="js/observation-engine.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>
|
<script src="js/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -7,16 +7,19 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// 1. Initialize Audio Subsystems
|
// 1. Initialize Audio Subsystems
|
||||||
const audioManager = new AudioManager();
|
const audioManager = new AudioManager();
|
||||||
|
const fncSystem = new FuturisticNoiceCancellation(audioManager);
|
||||||
|
fncSystem.bindUI();
|
||||||
|
window.fncSystem = fncSystem;
|
||||||
const hullDrone = new HullDroneSynth(audioManager);
|
const hullDrone = new HullDroneSynth(audioManager);
|
||||||
const warpCore = new WarpCoreSynth(audioManager);
|
const warpCore = new WarpCoreSynth(audioManager);
|
||||||
const lifeSupport = new LifeSupportSynth(audioManager);
|
const lifeSupport = new LifeSupportSynth(audioManager);
|
||||||
const telemetry = new TelemetrySynth(audioManager);
|
const telemetry = new TelemetrySynth(audioManager);
|
||||||
const alerts = new AlertSynth(audioManager);
|
const alerts = new AlertSynth(audioManager);
|
||||||
const whoniverseAudio = new WhataverseAudioSynth(audioManager);
|
const whataverseAudio = new WhataverseAudioSynth(audioManager);
|
||||||
const expandedAudio = new ExpandedSciFiAudioSynth(audioManager);
|
const expandedAudio = new ExpandedSciFiAudioSynth(audioManager);
|
||||||
const engineTransitions = new EngineTransitionSynth(audioManager);
|
const engineTransitions = new EngineTransitionSynth(audioManager);
|
||||||
window.expandedAudio = expandedAudio;
|
window.expandedAudio = expandedAudio;
|
||||||
window.whoniverseAudio = whoniverseAudio;
|
window.whataverseAudio = whataverseAudio;
|
||||||
window.engineTransitions = engineTransitions;
|
window.engineTransitions = engineTransitions;
|
||||||
const visualizer = new StarshipVisualizer(audioManager, warpCore);
|
const visualizer = new StarshipVisualizer(audioManager, warpCore);
|
||||||
const observationEngine = new ObservationEngine(audioManager, warpCore, alerts, hullDrone, lifeSupport);
|
const observationEngine = new ObservationEngine(audioManager, warpCore, alerts, hullDrone, lifeSupport);
|
||||||
@@ -94,6 +97,117 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const timerDisplay = document.getElementById('timer-display');
|
const timerDisplay = document.getElementById('timer-display');
|
||||||
const timerButtons = document.querySelectorAll('.btn-timer');
|
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
|
// 3. Universe Management & Switching
|
||||||
function initUniverseDropdown() {
|
function initUniverseDropdown() {
|
||||||
universeDropdown.innerHTML = '';
|
universeDropdown.innerHTML = '';
|
||||||
@@ -124,6 +238,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
activeUniverseId = universeId;
|
activeUniverseId = universeId;
|
||||||
universeDropdown.classList.remove('show');
|
universeDropdown.classList.remove('show');
|
||||||
|
if (window.generativeExperience) window.generativeExperience.onThemeChanged();
|
||||||
|
if (window.fncSystem) window.fncSystem.setTheme(universeId);
|
||||||
|
|
||||||
// Update Dropdown UI active class
|
// Update Dropdown UI active class
|
||||||
document.querySelectorAll('.universe-item').forEach(el => {
|
document.querySelectorAll('.universe-item').forEach(el => {
|
||||||
@@ -133,6 +249,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// Update Header and Theme Class
|
// Update Header and Theme Class
|
||||||
headerUniverseLabel.textContent = universe.shortCode;
|
headerUniverseLabel.textContent = universe.shortCode;
|
||||||
document.body.className = universe.themeClass;
|
document.body.className = universe.themeClass;
|
||||||
|
// Assigning className wholesale drops the TV presentation classes, so re-apply them.
|
||||||
|
applyTV();
|
||||||
headerMatrixLabel.textContent = universe.headerTitle;
|
headerMatrixLabel.textContent = universe.headerTitle;
|
||||||
|
|
||||||
// Custom titles per universe
|
// Custom titles per universe
|
||||||
@@ -143,7 +261,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
visLabel: 'INTERMIX CHAMBER // ACTIVE',
|
visLabel: 'INTERMIX CHAMBER // ACTIVE',
|
||||||
soundboard: 'LCARD KEYPAD'
|
soundboard: 'LCARD KEYPAD'
|
||||||
},
|
},
|
||||||
'whoniverse': {
|
'whataverse': {
|
||||||
sidebar: 'TARDIX CONSOLES',
|
sidebar: 'TARDIX CONSOLES',
|
||||||
visTitle: 'CENTRAL TIME ROTOR COLUMN',
|
visTitle: 'CENTRAL TIME ROTOR COLUMN',
|
||||||
visLabel: 'VORTEX ROTOR // OPERATIONAL',
|
visLabel: 'VORTEX ROTOR // OPERATIONAL',
|
||||||
@@ -288,7 +406,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// re-engages the ambience matching the currently selected preset.
|
// re-engages the ambience matching the currently selected preset.
|
||||||
function startPresetAmbience(presetId) {
|
function startPresetAmbience(presetId) {
|
||||||
expandedAudio.stopAllLoops();
|
expandedAudio.stopAllLoops();
|
||||||
whoniverseAudio.stopAmbientLoops();
|
whataverseAudio.stopAmbientLoops();
|
||||||
|
|
||||||
// Legacy Phase 1 loops, kept on their original presets
|
// Legacy Phase 1 loops, kept on their original presets
|
||||||
if (presetId === 'sta-sevastopol') {
|
if (presetId === 'sta-sevastopol') {
|
||||||
@@ -328,6 +446,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
activePresetId = presetId;
|
activePresetId = presetId;
|
||||||
window.activePresetId = presetId;
|
window.activePresetId = presetId;
|
||||||
|
if (window.fncSystem) window.fncSystem.resetLearning('preset change');
|
||||||
|
|
||||||
document.querySelectorAll('.preset-btn').forEach(b => {
|
document.querySelectorAll('.preset-btn').forEach(b => {
|
||||||
b.classList.toggle('active', b.getAttribute('data-preset-id') === presetId);
|
b.classList.toggle('active', b.getAttribute('data-preset-id') === presetId);
|
||||||
@@ -346,10 +465,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
syncSlidersToSynthParams();
|
syncSlidersToSynthParams();
|
||||||
|
|
||||||
expandedAudio.stopAllLoops();
|
expandedAudio.stopAllLoops();
|
||||||
whoniverseAudio.stopAmbientLoops();
|
whataverseAudio.stopAmbientLoops();
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
startPresetAmbience(presetId);
|
startPresetAmbience(presetId);
|
||||||
if (activeUniverseId === 'whoniverse') whoniverseAudio.synthesizeDematSwitch();
|
if (activeUniverseId === 'whataverse') whataverseAudio.synthesizeDematSwitch();
|
||||||
else if (activeUniverseId === 'industrial') expandedAudio.synthesizeDockingClamp();
|
else if (activeUniverseId === 'industrial') expandedAudio.synthesizeDockingClamp();
|
||||||
else telemetry.synthesizeLCARDSingleChirp();
|
else telemetry.synthesizeLCARDSingleChirp();
|
||||||
}
|
}
|
||||||
@@ -399,6 +518,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (!isPlaying) {
|
if (!isPlaying) {
|
||||||
isTransitioning = true;
|
isTransitioning = true;
|
||||||
await audioManager.resume();
|
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
|
// Flashing state while engaging
|
||||||
btnPlay.classList.add('engaging');
|
btnPlay.classList.add('engaging');
|
||||||
@@ -410,7 +533,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
engineTransitions.playStartup(engineProfile, ENGINE_TRANSITION_SECS);
|
engineTransitions.playStartup(engineProfile, ENGINE_TRANSITION_SECS);
|
||||||
|
|
||||||
// Acoustic chime / chirp feedback
|
// Acoustic chime / chirp feedback
|
||||||
if (activeUniverseId === 'whoniverse') whoniverseAudio.synthesizeTelepathicChime();
|
if (activeUniverseId === 'whataverse') whataverseAudio.synthesizeTelepathicChime();
|
||||||
else if (activeUniverseId === 'bioships') expandedAudio.synthesizeStarburst();
|
else if (activeUniverseId === 'bioships') expandedAudio.synthesizeStarburst();
|
||||||
else telemetry.synthesizeLCARDDoubleChirp();
|
else telemetry.synthesizeLCARDDoubleChirp();
|
||||||
|
|
||||||
@@ -434,6 +557,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (activePresetId) {
|
if (activePresetId) {
|
||||||
startPresetAmbience(activePresetId);
|
startPresetAmbience(activePresetId);
|
||||||
}
|
}
|
||||||
|
if (generativeExperience.ui.auto?.checked) generativeExperience.scheduleNext();
|
||||||
}, ENGINE_TRANSITION_SECS * 1000);
|
}, ENGINE_TRANSITION_SECS * 1000);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -448,7 +572,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// Layered & Staged Procedural Shutdown Script for this Profile
|
// Layered & Staged Procedural Shutdown Script for this Profile
|
||||||
engineTransitions.playShutdown(engineProfile, ENGINE_TRANSITION_SECS);
|
engineTransitions.playShutdown(engineProfile, ENGINE_TRANSITION_SECS);
|
||||||
|
|
||||||
if (activeUniverseId === 'whoniverse') whoniverseAudio.synthesizeLeverClunk();
|
if (activeUniverseId === 'whataverse') whataverseAudio.synthesizeLeverClunk();
|
||||||
else telemetry.synthesizeLCARDDoubleChirp();
|
else telemetry.synthesizeLCARDDoubleChirp();
|
||||||
|
|
||||||
// Stop engine sounds with smooth spool-down fade
|
// Stop engine sounds with smooth spool-down fade
|
||||||
@@ -457,10 +581,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
lifeSupport.stop(ENGINE_TRANSITION_SECS);
|
lifeSupport.stop(ENGINE_TRANSITION_SECS);
|
||||||
telemetry.stop();
|
telemetry.stop();
|
||||||
expandedAudio.stopAllLoops();
|
expandedAudio.stopAllLoops();
|
||||||
whoniverseAudio.stopAmbientLoops();
|
whataverseAudio.stopAmbientLoops();
|
||||||
alerts.stopAlert();
|
alerts.stopAlert();
|
||||||
whoniverseAudio.stopCloisterBell();
|
whataverseAudio.stopCloisterBell();
|
||||||
|
fncSystem.hold('alert', false);
|
||||||
resetAlertButtons();
|
resetAlertButtons();
|
||||||
|
generativeExperience.stop();
|
||||||
|
fncSystem.resetLearning('full stop');
|
||||||
|
fncSystem.stop();
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isPlaying = false;
|
isPlaying = false;
|
||||||
@@ -597,6 +725,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleUniverseEvent(type, btnElement) {
|
function handleUniverseEvent(type, btnElement) {
|
||||||
|
fncSystem.holdFor(6500, 'event');
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'warp':
|
case 'warp':
|
||||||
case 'quantum':
|
case 'quantum':
|
||||||
@@ -608,39 +737,45 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
case 'scramble':
|
case 'scramble':
|
||||||
if (alerts.activeAlert === 'red') {
|
if (alerts.activeAlert === 'red') {
|
||||||
alerts.stopAlert();
|
alerts.stopAlert();
|
||||||
|
fncSystem.hold('alert', false);
|
||||||
resetAlertButtons();
|
resetAlertButtons();
|
||||||
} else {
|
} else {
|
||||||
resetAlertButtons();
|
resetAlertButtons();
|
||||||
btnElement.classList.add('active-alert');
|
btnElement.classList.add('active-alert');
|
||||||
alerts.triggerRedAlert('tng');
|
alerts.triggerRedAlert('tng');
|
||||||
|
fncSystem.hold('alert', true);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'alert-yellow':
|
case 'alert-yellow':
|
||||||
if (alerts.activeAlert === 'yellow') {
|
if (alerts.activeAlert === 'yellow') {
|
||||||
alerts.stopAlert();
|
alerts.stopAlert();
|
||||||
|
fncSystem.hold('alert', false);
|
||||||
resetAlertButtons();
|
resetAlertButtons();
|
||||||
} else {
|
} else {
|
||||||
resetAlertButtons();
|
resetAlertButtons();
|
||||||
btnElement.classList.add('active-alert');
|
btnElement.classList.add('active-alert');
|
||||||
alerts.triggerYellowAlert();
|
alerts.triggerYellowAlert();
|
||||||
|
fncSystem.hold('alert', true);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'demat':
|
case 'demat':
|
||||||
whoniverseAudio.synthesizeDematCycle(4);
|
whataverseAudio.synthesizeDematCycle(4);
|
||||||
visualizer.spawnWarpPulses();
|
visualizer.spawnWarpPulses();
|
||||||
break;
|
break;
|
||||||
case 'vortex':
|
case 'vortex':
|
||||||
whoniverseAudio.synthesizeDematCycle(1);
|
whataverseAudio.synthesizeDematCycle(1);
|
||||||
visualizer.spawnWarpPulses();
|
visualizer.spawnWarpPulses();
|
||||||
break;
|
break;
|
||||||
case 'cloister':
|
case 'cloister':
|
||||||
if (whoniverseAudio.activeCloister) {
|
if (whataverseAudio.activeCloister) {
|
||||||
whoniverseAudio.stopCloisterBell();
|
whataverseAudio.stopCloisterBell();
|
||||||
|
fncSystem.hold('alert', false);
|
||||||
resetAlertButtons();
|
resetAlertButtons();
|
||||||
} else {
|
} else {
|
||||||
resetAlertButtons();
|
resetAlertButtons();
|
||||||
btnElement.classList.add('active-alert');
|
btnElement.classList.add('active-alert');
|
||||||
whoniverseAudio.triggerCloisterBell();
|
whataverseAudio.triggerCloisterBell();
|
||||||
|
fncSystem.hold('alert', true);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'epstein':
|
case 'epstein':
|
||||||
@@ -654,7 +789,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
expandedAudio.synthesizeDockingClamp();
|
expandedAudio.synthesizeDockingClamp();
|
||||||
break;
|
break;
|
||||||
case 'vent':
|
case 'vent':
|
||||||
whoniverseAudio.synthesizeFastReturn();
|
whataverseAudio.synthesizeFastReturn();
|
||||||
break;
|
break;
|
||||||
case 'starburst':
|
case 'starburst':
|
||||||
case 'biodefense':
|
case 'biodefense':
|
||||||
@@ -705,9 +840,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
telemetry.synthesizeLCARDSingleChirp();
|
telemetry.synthesizeLCARDSingleChirp();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
generativeExperience.describeExistingEvent(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSoundboardTrigger(id) {
|
function handleSoundboardTrigger(id) {
|
||||||
|
fncSystem.holdFor(2800, 'soundboard');
|
||||||
const universe = UniverseRegistry[activeUniverseId];
|
const universe = UniverseRegistry[activeUniverseId];
|
||||||
const preset = universe && universe.presets ? universe.presets[activePresetId] : null;
|
const preset = universe && universe.presets ? universe.presets[activePresetId] : null;
|
||||||
const era = preset ? preset.era : 'tng';
|
const era = preset ? preset.era : 'tng';
|
||||||
@@ -745,27 +882,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// Whataverse
|
// Whataverse
|
||||||
case 'btn-sonic':
|
case 'btn-sonic':
|
||||||
whoniverseAudio.synthesizeSonicScrewdriver();
|
whataverseAudio.synthesizeSonicScrewdriver();
|
||||||
break;
|
break;
|
||||||
case 'btn-lever-clunk':
|
case 'btn-lever-clunk':
|
||||||
whoniverseAudio.synthesizeLeverClunk();
|
whataverseAudio.synthesizeLeverClunk();
|
||||||
break;
|
break;
|
||||||
case 'btn-tardis-door':
|
case 'btn-tardix-door':
|
||||||
whoniverseAudio.synthesizeTARDIXDoor();
|
whataverseAudio.synthesizeTARDIXDoor();
|
||||||
break;
|
break;
|
||||||
case 'btn-fast-return':
|
case 'btn-fast-return':
|
||||||
whoniverseAudio.synthesizeFastReturn();
|
whataverseAudio.synthesizeFastReturn();
|
||||||
break;
|
break;
|
||||||
case 'btn-demat-switch':
|
case 'btn-demat-switch':
|
||||||
case 'btn-relay-click':
|
case 'btn-relay-click':
|
||||||
whoniverseAudio.synthesizeDematSwitch();
|
whataverseAudio.synthesizeDematSwitch();
|
||||||
break;
|
break;
|
||||||
case 'btn-telepathic':
|
case 'btn-telepathic':
|
||||||
case 'btn-telepathic-chime':
|
case 'btn-telepathic-chime':
|
||||||
whoniverseAudio.synthesizeTelepathicChime();
|
whataverseAudio.synthesizeTelepathicChime();
|
||||||
break;
|
break;
|
||||||
case 'btn-scanner':
|
case 'btn-scanner':
|
||||||
whoniverseAudio.synthesizeDematSwitch();
|
whataverseAudio.synthesizeDematSwitch();
|
||||||
telemetry.synthesizeSensorSweep();
|
telemetry.synthesizeSensorSweep();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -786,7 +923,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
expandedAudio.synthesizeStationMedicalPing();
|
expandedAudio.synthesizeStationMedicalPing();
|
||||||
break;
|
break;
|
||||||
case 'btn-tram-gong':
|
case 'btn-tram-gong':
|
||||||
whoniverseAudio.synthesizeCloisterStrike();
|
whataverseAudio.synthesizeCloisterStrike();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Deep Space
|
// Deep Space
|
||||||
@@ -857,10 +994,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
expandedAudio.synthesizeFabricatorShimmer();
|
expandedAudio.synthesizeFabricatorShimmer();
|
||||||
break;
|
break;
|
||||||
case 'btn-scanner-activate':
|
case 'btn-scanner-activate':
|
||||||
whoniverseAudio.synthesizeScannerActivate();
|
whataverseAudio.synthesizeScannerActivate();
|
||||||
break;
|
break;
|
||||||
case 'btn-type-clatter':
|
case 'btn-type-clatter':
|
||||||
whoniverseAudio.synthesizeTypeClatter();
|
whataverseAudio.synthesizeTypeClatter();
|
||||||
break;
|
break;
|
||||||
case 'btn-vorlon-song':
|
case 'btn-vorlon-song':
|
||||||
expandedAudio.synthesizeVorlanSingingSwell();
|
expandedAudio.synthesizeVorlanSingingSwell();
|
||||||
@@ -1079,7 +1216,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
function obsThemeSplineScale(theme) {
|
function obsThemeSplineScale(theme) {
|
||||||
return ({
|
return ({
|
||||||
starfleet: 0.60,
|
starfleet: 0.60,
|
||||||
whoniverse: 1.05,
|
whataverse: 1.05,
|
||||||
industrial: 0.48,
|
industrial: 0.48,
|
||||||
bioships: 1.28,
|
bioships: 1.28,
|
||||||
retrofuture: 0.72,
|
retrofuture: 0.72,
|
||||||
@@ -1439,7 +1576,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
</g>`;
|
</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>`;
|
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}
|
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})}
|
${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 = {
|
const generator = {
|
||||||
starfleet: generateStarflightActivity,
|
starfleet: generateStarflightActivity,
|
||||||
whoniverse: generateWhataverseActivity,
|
whataverse: generateWhataverseActivity,
|
||||||
industrial: generateIndustrialActivity,
|
industrial: generateIndustrialActivity,
|
||||||
bioships: generateBioshipActivity,
|
bioships: generateBioshipActivity,
|
||||||
retrofuture: generateRetroActivity,
|
retrofuture: generateRetroActivity,
|
||||||
@@ -2546,7 +2683,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
const observationTextVocabulary = {
|
const observationTextVocabulary = {
|
||||||
starfleet: ['NOMINAL','PASSIVE','SCANNING','CORRELATING','TRACKING','VERIFIED','STABLE'],
|
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'],
|
industrial: ['SECURE FEED','MONITORING','SIGNAL HOLD','CYCLING','LOCAL CHECK','STABLE'],
|
||||||
bioships: ['QUIESCENT','RESPONDING','ADAPTING','RESONANT','SYNCHRONIZED','PULSING'],
|
bioships: ['QUIESCENT','RESPONDING','ADAPTING','RESONANT','SYNCHRONIZED','PULSING'],
|
||||||
retrofuture: ['READY','READ','PROCESS','HOLD','TRACK','STABLE'],
|
retrofuture: ['READY','READ','PROCESS','HOLD','TRACK','STABLE'],
|
||||||
@@ -3901,6 +4038,62 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
function renderObservationOutlaw() {
|
function renderObservationOutlaw() {
|
||||||
const stars = makeStars(115, 1700, 900, '#fbcfe8', 1710);
|
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(`
|
return svgFrame(`
|
||||||
<rect width="1600" height="900" fill="#09040c"/>
|
<rect width="1600" height="900" fill="#09040c"/>
|
||||||
<g style="animation:obs-drift-left 95s linear infinite" opacity=".55">${stars}</g>
|
<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"/>
|
<path d="M630 390h120v-45M970 390H850v-45M630 540h120v45M970 540H850v45" stroke-width="4"/>
|
||||||
<circle cx="800" cy="465" r="95" opacity=".25"/>
|
<circle cx="800" cy="465" r="95" opacity=".25"/>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<g transform="translate(120 210)">
|
<g transform="translate(120 210)">
|
||||||
<g class="obs-crt-jump">
|
<g class="obs-crt-jump">
|
||||||
<rect width="290" height="170" rx="8" fill="#130919" stroke="#8b5cf6" stroke-width="3"/>
|
<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"/>
|
<circle cx="225" cy="68" r="5" fill="#ec4899" class="obs-blink"/>
|
||||||
</g>
|
</g>
|
||||||
</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">
|
<g fill="#f472b6" font-family="monospace" opacity=".72">
|
||||||
<text x="1190" y="675" font-size="17">ROUTE: MANUAL</text>
|
<text x="1190" y="675" font-size="17">ROUTE: MANUAL</text>
|
||||||
<text x="1190" y="702" font-size="14">TRANSPONDER: INTERMITTENT</text>
|
<text x="1190" y="702" font-size="14">TRANSPONDER: INTERMITTENT</text>
|
||||||
@@ -4061,7 +4265,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
function getObservationSvg(universeId) {
|
function getObservationSvg(universeId) {
|
||||||
switch (universeId) {
|
switch (universeId) {
|
||||||
case 'starfleet': return renderObservationStarflight();
|
case 'starfleet': return renderObservationStarflight();
|
||||||
case 'whoniverse': return renderObservationWhataverse();
|
case 'whataverse': return renderObservationWhataverse();
|
||||||
case 'industrial': return renderObservationIndustrial();
|
case 'industrial': return renderObservationIndustrial();
|
||||||
case 'bioships': return renderObservationBioships();
|
case 'bioships': return renderObservationBioships();
|
||||||
case 'retrofuture': return renderObservationRetro();
|
case 'retrofuture': return renderObservationRetro();
|
||||||
@@ -4097,6 +4301,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
function enterObservation() {
|
function enterObservation() {
|
||||||
if (observationActive) return;
|
if (observationActive) return;
|
||||||
|
generativeExperience.prepareExperience().catch(err => console.warn('Generative preload failed', err));
|
||||||
observationActive = true;
|
observationActive = true;
|
||||||
observationOverlay.classList.add('active');
|
observationOverlay.classList.add('active');
|
||||||
observationOverlay.setAttribute('aria-hidden', 'false');
|
observationOverlay.setAttribute('aria-hidden', 'false');
|
||||||
@@ -4226,7 +4431,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
alerts.stopAlert();
|
alerts.stopAlert();
|
||||||
whoniverseAudio.stopCloisterBell();
|
whataverseAudio.stopCloisterBell();
|
||||||
resetAlertButtons();
|
resetAlertButtons();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+561
-54
@@ -180,6 +180,476 @@ class AudioManager {
|
|||||||
|
|
||||||
window.AudioManager = 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
|
* Hull Drone & Environmental Sub-Bass Synthesizer
|
||||||
@@ -278,7 +748,8 @@ class HullDroneSynth {
|
|||||||
// Connect Graph
|
// Connect Graph
|
||||||
this.filterNode.connect(filterStage2);
|
this.filterNode.connect(filterStage2);
|
||||||
filterStage2.connect(this.gainNode);
|
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
|
// Start Sources
|
||||||
this.subOsc1.start();
|
this.subOsc1.start();
|
||||||
@@ -306,6 +777,7 @@ class HullDroneSynth {
|
|||||||
this.stoppingTimeout = null;
|
this.stoppingTimeout = null;
|
||||||
}
|
}
|
||||||
if (this.nodes.length > 0) {
|
if (this.nodes.length > 0) {
|
||||||
|
if (window.fncSystem) window.fncSystem.detachSource('hull');
|
||||||
try {
|
try {
|
||||||
if (this.subOsc1) this.subOsc1.stop();
|
if (this.subOsc1) this.subOsc1.stop();
|
||||||
if (this.subOsc2) this.subOsc2.stop();
|
if (this.subOsc2) this.subOsc2.stop();
|
||||||
@@ -457,7 +929,8 @@ class WarpCoreSynth {
|
|||||||
this.filterNode.connect(this.gainNode);
|
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.carrier.start();
|
||||||
this.subOsc.start();
|
this.subOsc.start();
|
||||||
@@ -611,6 +1084,7 @@ class WarpCoreSynth {
|
|||||||
this.pulseInterval = null;
|
this.pulseInterval = null;
|
||||||
}
|
}
|
||||||
if (this.nodes.length > 0) {
|
if (this.nodes.length > 0) {
|
||||||
|
if (window.fncSystem) window.fncSystem.detachSource('worp');
|
||||||
try {
|
try {
|
||||||
if (this.carrier) this.carrier.stop();
|
if (this.carrier) this.carrier.stop();
|
||||||
if (this.subOsc) this.subOsc.stop();
|
if (this.subOsc) this.subOsc.stop();
|
||||||
@@ -724,7 +1198,8 @@ class LifeSupportSynth {
|
|||||||
delayRight.connect(merger, 0, 1); // Right delayed channel
|
delayRight.connect(merger, 0, 1); // Right delayed channel
|
||||||
|
|
||||||
merger.connect(this.gainNode);
|
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();
|
this.noiseSource.start();
|
||||||
airflowLfo.start();
|
airflowLfo.start();
|
||||||
@@ -752,6 +1227,7 @@ class LifeSupportSynth {
|
|||||||
this.stoppingTimeout = null;
|
this.stoppingTimeout = null;
|
||||||
}
|
}
|
||||||
if (this.nodes.length > 0) {
|
if (this.nodes.length > 0) {
|
||||||
|
if (window.fncSystem) window.fncSystem.detachSource('lifeSupport');
|
||||||
try {
|
try {
|
||||||
if (this.noiseSource) this.noiseSource.stop();
|
if (this.noiseSource) this.noiseSource.stop();
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
@@ -811,7 +1287,7 @@ class TelemetrySynth {
|
|||||||
this.params = {
|
this.params = {
|
||||||
volume: 0.4,
|
volume: 0.4,
|
||||||
density: 0.5, // How often background telemetry chirps occur (0 = off, 1 = busy bridge)
|
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
|
reverbMix: 0.25
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -882,7 +1358,10 @@ class TelemetrySynth {
|
|||||||
// Calculate delay inversely proportional to density (2s to 12s)
|
// Calculate delay inversely proportional to density (2s to 12s)
|
||||||
const baseDelay = 12000 * (1.05 - this.params.density);
|
const baseDelay = 12000 * (1.05 - this.params.density);
|
||||||
const jitter = Math.random() * 4000;
|
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.schedulerTimer = setTimeout(() => {
|
||||||
this.playRandomTelemetrySound();
|
this.playRandomTelemetrySound();
|
||||||
@@ -894,8 +1373,11 @@ class TelemetrySynth {
|
|||||||
if (this.isMuted || this.params.volume <= 0.01 || !this.am.ctx) return;
|
if (this.isMuted || this.params.volume <= 0.01 || !this.am.ctx) return;
|
||||||
|
|
||||||
switch (this.params.era) {
|
switch (this.params.era) {
|
||||||
case 'who':
|
case 'whataverse':
|
||||||
this.synthesizeWhoMechanicalTelemetry_();
|
case 'tardix':
|
||||||
|
case 'what':
|
||||||
|
case 'who': // legacy preset compatibility
|
||||||
|
this.synthesizeWhataverseMechanicalTelemetry_();
|
||||||
break;
|
break;
|
||||||
case 'bioship':
|
case 'bioship':
|
||||||
this.synthesizeBioshipOrganicTelemetry_();
|
this.synthesizeBioshipOrganicTelemetry_();
|
||||||
@@ -1298,80 +1780,107 @@ class TelemetrySynth {
|
|||||||
* (no electronic beeps). All transients scheduled on absolute ctx time so
|
* (no electronic beeps). All transients scheduled on absolute ctx time so
|
||||||
* the single scheduler timeout can never orphan nodes.
|
* the single scheduler timeout can never orphan nodes.
|
||||||
*/
|
*/
|
||||||
synthesizeWhoMechanicalTelemetry_() {
|
synthesizeWhataverseMechanicalTelemetry_() {
|
||||||
this.init();
|
this.init();
|
||||||
const ctx = this.am.ctx;
|
const ctx = this.am.ctx;
|
||||||
if (!ctx || !this.gainNode) return;
|
if (!ctx || !this.gainNode) return;
|
||||||
|
|
||||||
const now = ctx.currentTime;
|
const now = ctx.currentTime;
|
||||||
const strikeCount = 2 + Math.floor(Math.random() * 4); // 2-5 transients
|
// Whataverse telemetry should read as an electromechanical console at room
|
||||||
let cursor = Math.random() * 0.12;
|
// 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 noiseBurst = (t, type, freq, q, peak, dur) => {
|
||||||
const toggleSnap = (t, peak) => {
|
const buf = this.am.createNoiseBuffer(type, Math.max(0.12, dur + 0.04));
|
||||||
const buf = this.am.createNoiseBuffer('white', 0.06);
|
const src = ctx.createBufferSource();
|
||||||
const noise = ctx.createBufferSource();
|
src.buffer = buf;
|
||||||
noise.buffer = buf;
|
const bp = ctx.createBiquadFilter();
|
||||||
const bpLo = ctx.createBiquadFilter();
|
bp.type = 'bandpass';
|
||||||
bpLo.type = 'bandpass';
|
bp.frequency.setValueAtTime(freq, t);
|
||||||
bpLo.frequency.setValueAtTime(300 + Math.random() * 200, t);
|
bp.Q.setValueAtTime(q, 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 env = ctx.createGain();
|
const env = ctx.createGain();
|
||||||
env.gain.setValueAtTime(0.001, t);
|
env.gain.setValueAtTime(0.001, t);
|
||||||
env.gain.linearRampToValueAtTime(peak, t + 0.004);
|
env.gain.linearRampToValueAtTime(peak, t + 0.004);
|
||||||
env.gain.exponentialRampToValueAtTime(0.001, t + 0.02 + Math.random() * 0.025);
|
env.gain.exponentialRampToValueAtTime(0.001, t + dur);
|
||||||
noise.connect(bpLo);
|
src.connect(bp);
|
||||||
bpLo.connect(env);
|
bp.connect(env);
|
||||||
noise.connect(bpHi);
|
|
||||||
bpHi.connect(env);
|
|
||||||
env.connect(this.gainNode);
|
env.connect(this.gainNode);
|
||||||
noise.start(t);
|
src.start(t);
|
||||||
noise.stop(t + 0.06);
|
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 springLever = (t, peak) => {
|
||||||
const osc = ctx.createOscillator();
|
const osc = ctx.createOscillator();
|
||||||
osc.type = 'square';
|
osc.type = 'triangle';
|
||||||
const f0 = 60 + Math.random() * 60;
|
const f0 = 150 + Math.random() * 90;
|
||||||
osc.frequency.setValueAtTime(f0, t);
|
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();
|
const env = ctx.createGain();
|
||||||
env.gain.setValueAtTime(peak, t);
|
env.gain.setValueAtTime(0.001, t);
|
||||||
env.gain.exponentialRampToValueAtTime(0.001, t + 0.03 + Math.random() * 0.02);
|
env.gain.linearRampToValueAtTime(peak, t + 0.004);
|
||||||
|
env.gain.exponentialRampToValueAtTime(0.001, t + 0.11);
|
||||||
osc.connect(env);
|
osc.connect(env);
|
||||||
env.connect(this.gainNode);
|
env.connect(this.gainNode);
|
||||||
osc.start(t);
|
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
|
// Rotary selector: two or three obvious detents rather than one sine ping.
|
||||||
const rotaryClunk = (t, peak) => {
|
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();
|
const osc = ctx.createOscillator();
|
||||||
osc.type = 'sine';
|
osc.type = 'square';
|
||||||
const f0 = 180 + Math.random() * 260;
|
osc.frequency.setValueAtTime(190 + Math.random() * 70, t);
|
||||||
osc.frequency.setValueAtTime(f0, t);
|
osc.frequency.exponentialRampToValueAtTime(65 + Math.random() * 20, t + 0.065);
|
||||||
osc.frequency.exponentialRampToValueAtTime(f0 * 0.7, t + 0.06);
|
|
||||||
const env = ctx.createGain();
|
const env = ctx.createGain();
|
||||||
env.gain.setValueAtTime(0.001, t);
|
env.gain.setValueAtTime(0.001, t);
|
||||||
env.gain.linearRampToValueAtTime(peak, t + 0.003);
|
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);
|
osc.connect(env);
|
||||||
env.connect(this.gainNode);
|
env.connect(this.gainNode);
|
||||||
osc.start(t);
|
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();
|
const r = Math.random();
|
||||||
if (r < 0.5) toggleSnap(cursor, 0.08 + Math.random() * 0.08);
|
const peak = 0.16 + Math.random() * 0.12;
|
||||||
else if (r < 0.8) springLever(cursor, 0.07 + Math.random() * 0.06);
|
if (r < 0.34) toggleSnap(t, peak);
|
||||||
else rotaryClunk(cursor, 0.09 + Math.random() * 0.07);
|
else if (r < 0.60) springLever(t, peak);
|
||||||
cursor += 0.04 + Math.random() * 0.14;
|
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 'cardassian': this._startupCardassian(now, s); break;
|
||||||
case 'tos': this._startupTOS(now, s); break;
|
case 'tos': this._startupTOS(now, s); break;
|
||||||
case 'nx': this._startupNX(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 'industrial': this._startupIndustrial(now, s); break;
|
||||||
case 'bioship': this._startupBioship(now, s); break;
|
case 'bioship': this._startupBioship(now, s); break;
|
||||||
case 'retrofuture': this._startupRetrofuture(now, s); break;
|
case 'retrofuture': this._startupRetrofuture(now, s); break;
|
||||||
@@ -6121,7 +6630,7 @@ class EngineTransitionSynth {
|
|||||||
case 'cardassian': this._shutdownCardassian(now, s); break;
|
case 'cardassian': this._shutdownCardassian(now, s); break;
|
||||||
case 'tos': this._shutdownTOS(now, s); break;
|
case 'tos': this._shutdownTOS(now, s); break;
|
||||||
case 'nx': this._shutdownNX(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 'industrial': this._shutdownIndustrial(now, s); break;
|
||||||
case 'bioship': this._shutdownBioship(now, s); break;
|
case 'bioship': this._shutdownBioship(now, s); break;
|
||||||
case 'retrofuture': this._shutdownRetrofuture(now, s); break;
|
case 'retrofuture': this._shutdownRetrofuture(now, s); break;
|
||||||
@@ -6378,5 +6887,3 @@ class EngineTransitionSynth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.EngineTransitionSynth = EngineTransitionSynth;
|
window.EngineTransitionSynth = EngineTransitionSynth;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+41
-42
@@ -154,7 +154,7 @@ const UniverseRegistry = {
|
|||||||
shortCode: 'STARFLIGHT',
|
shortCode: 'STARFLIGHT',
|
||||||
icon: '★',
|
icon: '★',
|
||||||
themeClass: 'theme-lcars-tng',
|
themeClass: 'theme-lcars-tng',
|
||||||
headerTitle: 'STARFLIGHT SOUND MATRIX v2.5',
|
headerTitle: 'STARFLIGHT SOUND MATRIX v2.9',
|
||||||
visualizer: 'intermix-cascade',
|
visualizer: 'intermix-cascade',
|
||||||
events: [
|
events: [
|
||||||
{ id: 'btn-warp-jump', label: 'WARP JUMP', type: 'warp' },
|
{ id: 'btn-warp-jump', label: 'WARP JUMP', type: 'warp' },
|
||||||
@@ -178,12 +178,12 @@ const UniverseRegistry = {
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
// 2. THE WHATAVERSE (Doctor Who TARDIX)
|
// 2. THE WHATAVERSE (Doctor Who TARDIX)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
'whoniverse': {
|
'whataverse': {
|
||||||
id: 'whoniverse',
|
id: 'whataverse',
|
||||||
name: 'THE WHATAVERSE',
|
name: 'THE WHATAVERSE',
|
||||||
shortCode: 'T.A.R.D.I.X.',
|
shortCode: 'T.A.R.D.I.X.',
|
||||||
icon: '⌛',
|
icon: '⌛',
|
||||||
themeClass: 'theme-whoniverse-tardis',
|
themeClass: 'theme-whataverse-tardix',
|
||||||
headerTitle: 'TYPE 4D TT CAPSULE MATRIX',
|
headerTitle: 'TYPE 4D TT CAPSULE MATRIX',
|
||||||
visualizer: 'time-rotor',
|
visualizer: 'time-rotor',
|
||||||
events: [
|
events: [
|
||||||
@@ -194,7 +194,7 @@ const UniverseRegistry = {
|
|||||||
soundboard: [
|
soundboard: [
|
||||||
{ id: 'btn-sonic', label: 'ACOUSTIC SCREWDRIVER' },
|
{ id: 'btn-sonic', label: 'ACOUSTIC SCREWDRIVER' },
|
||||||
{ id: 'btn-lever-clunk', label: 'LEVER-THROW CLUNK' },
|
{ id: 'btn-lever-clunk', label: 'LEVER-THROW CLUNK' },
|
||||||
{ id: 'btn-tardis-door', label: 'POLICE BOX WOODEN DOOR' },
|
{ id: 'btn-tardix-door', label: 'POLICE BOX WOODEN DOOR' },
|
||||||
{ id: 'btn-fast-return', label: 'FAST-RETURN LEVER' },
|
{ id: 'btn-fast-return', label: 'FAST-RETURN LEVER' },
|
||||||
{ id: 'btn-demat-switch', label: 'DEMAT SWITCH' },
|
{ id: 'btn-demat-switch', label: 'DEMAT SWITCH' },
|
||||||
{ id: 'btn-telepathic', label: 'TELEPATHIC CHIME' },
|
{ id: 'btn-telepathic', label: 'TELEPATHIC CHIME' },
|
||||||
@@ -202,89 +202,89 @@ const UniverseRegistry = {
|
|||||||
{ id: 'btn-type-clatter', label: 'CONSOLE TYPE CLATTER' }
|
{ id: 'btn-type-clatter', label: 'CONSOLE TYPE CLATTER' }
|
||||||
],
|
],
|
||||||
presets: {
|
presets: {
|
||||||
'tardis-1963': {
|
'tardix-1963': {
|
||||||
id: 'tardis-1963',
|
id: 'tardix-1963',
|
||||||
name: '1962 Classic Console (1st / 2nd Doctors)',
|
name: '1962 Classic Console (1st / 2nd Doctors)',
|
||||||
era: 'classic',
|
era: 'classic',
|
||||||
theme: 'whoniverse-tardis',
|
theme: 'whataverse-tardix',
|
||||||
alertType: 'cloister',
|
alertType: 'cloister',
|
||||||
description: 'Sterile white room tone, high-frequency 2-tone oscillating rotor pulse, and mechanical telephone relays.',
|
description: 'Sterile white room tone, high-frequency 2-tone oscillating rotor pulse, and mechanical telephone relays.',
|
||||||
hull: { volume: 0.5, baseFreq: 68, filterCutoff: 150, resonance: 2.8, noiseMix: 0.35, harmonicSpread: 1.04 },
|
hull: { volume: 0.5, baseFreq: 68, filterCutoff: 150, resonance: 2.8, noiseMix: 0.35, harmonicSpread: 1.04 },
|
||||||
warp: { volume: 0.65, bpm: 52, carrierFreq: 96, filterCutoff: 210, pulseShape: 'tos', resonance: 3.5, swirlMix: 0.2 },
|
warp: { volume: 0.65, bpm: 52, carrierFreq: 96, filterCutoff: 210, pulseShape: 'tos', resonance: 3.5, swirlMix: 0.2 },
|
||||||
lifeSupport: { volume: 0.45, noiseType: 'pink', highpassFreq: 220, lowpassFreq: 1800, airflowModSpeed: 0.08, airflowModDepth: 0.06 },
|
lifeSupport: { volume: 0.45, noiseType: 'pink', highpassFreq: 220, lowpassFreq: 1800, airflowModSpeed: 0.08, airflowModDepth: 0.06 },
|
||||||
telemetry: { volume: 0.55, density: 0.6, era: 'who' }
|
telemetry: { volume: 0.55, density: 0.6, era: 'whataverse' }
|
||||||
},
|
},
|
||||||
'tardis-victorian': {
|
'tardix-victorian': {
|
||||||
id: 'tardis-victorian',
|
id: 'tardix-victorian',
|
||||||
name: 'Edwardian Secondary Console (4th Doctor)',
|
name: 'Edwardian Secondary Console (4th Doctor)',
|
||||||
era: 'classic',
|
era: 'classic',
|
||||||
theme: 'whoniverse-tardis',
|
theme: 'whataverse-tardix',
|
||||||
alertType: 'cloister',
|
alertType: 'cloister',
|
||||||
description: 'Warm wood-paneled room acoustic damping, rhythmic ticking clockwork, quiet fire hearth, and brass gears.',
|
description: 'Warm wood-paneled room acoustic damping, rhythmic ticking clockwork, quiet fire hearth, and brass gears.',
|
||||||
hull: { volume: 0.65, baseFreq: 44, filterCutoff: 85, resonance: 1.8, noiseMix: 0.45, harmonicSpread: 1.015 },
|
hull: { volume: 0.65, baseFreq: 44, filterCutoff: 85, resonance: 1.8, noiseMix: 0.45, harmonicSpread: 1.015 },
|
||||||
warp: { volume: 0.3, bpm: 40, carrierFreq: 50, filterCutoff: 120, pulseShape: 'nx', resonance: 2.0, swirlMix: 0.15 },
|
warp: { volume: 0.3, bpm: 40, carrierFreq: 50, filterCutoff: 120, pulseShape: 'nx', resonance: 2.0, swirlMix: 0.15 },
|
||||||
lifeSupport: { volume: 0.55, noiseType: 'brown', highpassFreq: 110, lowpassFreq: 1200, airflowModSpeed: 0.07, airflowModDepth: 0.12 },
|
lifeSupport: { volume: 0.55, noiseType: 'brown', highpassFreq: 110, lowpassFreq: 1200, airflowModSpeed: 0.07, airflowModDepth: 0.12 },
|
||||||
telemetry: { volume: 0.35, density: 0.4, era: 'who' }
|
telemetry: { volume: 0.35, density: 0.4, era: 'whataverse' }
|
||||||
},
|
},
|
||||||
'tardis-coral': {
|
'tardix-coral': {
|
||||||
id: 'tardis-coral',
|
id: 'tardix-coral',
|
||||||
name: 'The Coral Living TARDIX (9th / 10th Doctors)',
|
name: 'The Coral Living TARDIX (9th / 10th Doctors)',
|
||||||
era: 'revival',
|
era: 'revival',
|
||||||
theme: 'whoniverse-tardis',
|
theme: 'whataverse-tardix',
|
||||||
alertType: 'cloister',
|
alertType: 'cloister',
|
||||||
description: 'Deep organic heartbeat thrum, bicycle pump squeaks, rattling mechanics, and steaming thermal exhaust.',
|
description: 'Deep organic heartbeat thrum, bicycle pump squeaks, rattling mechanics, and steaming thermal exhaust.',
|
||||||
hull: { volume: 0.75, baseFreq: 52, filterCutoff: 135, resonance: 3.2, noiseMix: 0.5, harmonicSpread: 1.03 },
|
hull: { volume: 0.75, baseFreq: 52, filterCutoff: 135, resonance: 3.2, noiseMix: 0.5, harmonicSpread: 1.03 },
|
||||||
warp: { volume: 0.9, bpm: 48, carrierFreq: 64, filterCutoff: 230, pulseShape: 'tng', resonance: 4.0, swirlMix: 0.45 },
|
warp: { volume: 0.9, bpm: 48, carrierFreq: 64, filterCutoff: 230, pulseShape: 'tng', resonance: 4.0, swirlMix: 0.45 },
|
||||||
lifeSupport: { volume: 0.6, noiseType: 'pink', highpassFreq: 160, lowpassFreq: 1700, airflowModSpeed: 0.18, airflowModDepth: 0.15 },
|
lifeSupport: { volume: 0.6, noiseType: 'pink', highpassFreq: 160, lowpassFreq: 1700, airflowModSpeed: 0.18, airflowModDepth: 0.15 },
|
||||||
telemetry: { volume: 0.45, density: 0.5, era: 'who' }
|
telemetry: { volume: 0.45, density: 0.5, era: 'whataverse' }
|
||||||
},
|
},
|
||||||
'tardis-copper': {
|
'tardix-copper': {
|
||||||
id: 'tardis-copper',
|
id: 'tardix-copper',
|
||||||
name: 'The Bronze Workshop (11th Doctor)',
|
name: 'The Bronze Workshop (11th Doctor)',
|
||||||
era: 'revival',
|
era: 'revival',
|
||||||
theme: 'whoniverse-tardis',
|
theme: 'whataverse-tardix',
|
||||||
alertType: 'cloister',
|
alertType: 'cloister',
|
||||||
description: 'Bustling kinetic atmosphere, whirring brass gyroscopes, bubbling conduits, and typewriter key clatter.',
|
description: 'Bustling kinetic atmosphere, whirring brass gyroscopes, bubbling conduits, and typewriter key clatter.',
|
||||||
hull: { volume: 0.6, baseFreq: 56, filterCutoff: 125, resonance: 2.5, noiseMix: 0.4, harmonicSpread: 1.02 },
|
hull: { volume: 0.6, baseFreq: 56, filterCutoff: 125, resonance: 2.5, noiseMix: 0.4, harmonicSpread: 1.02 },
|
||||||
warp: { volume: 0.7, bpm: 62, carrierFreq: 72, filterCutoff: 210, pulseShape: 'voyager', resonance: 3.6, swirlMix: 0.35 },
|
warp: { volume: 0.7, bpm: 62, carrierFreq: 72, filterCutoff: 210, pulseShape: 'voyager', resonance: 3.6, swirlMix: 0.35 },
|
||||||
lifeSupport: { volume: 0.65, noiseType: 'white', highpassFreq: 220, lowpassFreq: 2200, airflowModSpeed: 0.15, airflowModDepth: 0.1 },
|
lifeSupport: { volume: 0.65, noiseType: 'white', highpassFreq: 220, lowpassFreq: 2200, airflowModSpeed: 0.15, airflowModDepth: 0.1 },
|
||||||
telemetry: { volume: 0.55, density: 0.7, era: 'who' }
|
telemetry: { volume: 0.55, density: 0.7, era: 'whataverse' }
|
||||||
},
|
},
|
||||||
'tardis-cold-machine': {
|
'tardix-cold-machine': {
|
||||||
id: 'tardis-cold-machine',
|
id: 'tardix-cold-machine',
|
||||||
name: 'The Cool Machine (12th Doctor)',
|
name: 'The Cool Machine (12th Doctor)',
|
||||||
era: 'revival',
|
era: 'revival',
|
||||||
theme: 'whoniverse-tardis',
|
theme: 'whataverse-tardix',
|
||||||
alertType: 'cloister',
|
alertType: 'cloister',
|
||||||
description: 'Studious acoustic focus, spinning ceiling planetary rings, electric neon tube hum, and authoritative switches.',
|
description: 'Studious acoustic focus, spinning ceiling planetary rings, electric neon tube hum, and authoritative switches.',
|
||||||
hull: { volume: 0.6, baseFreq: 50, filterCutoff: 110, resonance: 2.2, noiseMix: 0.38, harmonicSpread: 1.02 },
|
hull: { volume: 0.6, baseFreq: 50, filterCutoff: 110, resonance: 2.2, noiseMix: 0.38, harmonicSpread: 1.02 },
|
||||||
warp: { volume: 0.75, bpm: 56, carrierFreq: 68, filterCutoff: 190, pulseShape: 'tng', resonance: 3.2, swirlMix: 0.3 },
|
warp: { volume: 0.75, bpm: 56, carrierFreq: 68, filterCutoff: 190, pulseShape: 'tng', resonance: 3.2, swirlMix: 0.3 },
|
||||||
lifeSupport: { volume: 0.5, noiseType: 'pink', highpassFreq: 200, lowpassFreq: 1600, airflowModSpeed: 0.12, airflowModDepth: 0.08 },
|
lifeSupport: { volume: 0.5, noiseType: 'pink', highpassFreq: 200, lowpassFreq: 1600, airflowModSpeed: 0.12, airflowModDepth: 0.08 },
|
||||||
telemetry: { volume: 0.4, density: 0.5, era: 'who' }
|
telemetry: { volume: 0.4, density: 0.5, era: 'whataverse' }
|
||||||
},
|
},
|
||||||
'tardis-crystal': {
|
'tardix-crystal': {
|
||||||
id: 'tardis-crystal',
|
id: 'tardix-crystal',
|
||||||
name: 'The Humming Crystal Console (13th Doctor)',
|
name: 'The Humming Crystal Console (13th Doctor)',
|
||||||
era: 'revival',
|
era: 'revival',
|
||||||
theme: 'whoniverse-tardis',
|
theme: 'whataverse-tardix',
|
||||||
alertType: 'cloister',
|
alertType: 'cloister',
|
||||||
description: 'High-resonance singing bowl quartz harmonics, tectonic subterranean rumble, and kinetic crystal crackles.',
|
description: 'High-resonance singing bowl quartz harmonics, tectonic subterranean rumble, and kinetic crystal crackles.',
|
||||||
hull: { volume: 0.7, baseFreq: 46, filterCutoff: 100, resonance: 3.5, noiseMix: 0.5, harmonicSpread: 1.04 },
|
hull: { volume: 0.7, baseFreq: 46, filterCutoff: 100, resonance: 3.5, noiseMix: 0.5, harmonicSpread: 1.04 },
|
||||||
warp: { volume: 0.8, bpm: 44, carrierFreq: 88, filterCutoff: 260, pulseShape: 'defiant', resonance: 4.4, swirlMix: 0.4 },
|
warp: { volume: 0.8, bpm: 44, carrierFreq: 88, filterCutoff: 260, pulseShape: 'defiant', resonance: 4.4, swirlMix: 0.4 },
|
||||||
lifeSupport: { volume: 0.55, noiseType: 'pink', highpassFreq: 180, lowpassFreq: 1900, airflowModSpeed: 0.16, airflowModDepth: 0.14 },
|
lifeSupport: { volume: 0.55, noiseType: 'pink', highpassFreq: 180, lowpassFreq: 1900, airflowModSpeed: 0.16, airflowModDepth: 0.14 },
|
||||||
telemetry: { volume: 0.5, density: 0.6, era: 'who' }
|
telemetry: { volume: 0.5, density: 0.6, era: 'whataverse' }
|
||||||
},
|
},
|
||||||
'tardis-infinite-white': {
|
'tardix-infinite-white': {
|
||||||
id: 'tardis-infinite-white',
|
id: 'tardix-infinite-white',
|
||||||
name: 'The Endless White (14th / 15th Doctors)',
|
name: 'The Endless White (14th / 15th Doctors)',
|
||||||
era: 'modern',
|
era: 'modern',
|
||||||
theme: 'whoniverse-tardis',
|
theme: 'whataverse-tardix',
|
||||||
alertType: 'cloister',
|
alertType: 'cloister',
|
||||||
description: 'Vast cathedral-scale spatial reverberation, pristine futuristic pulses, and smooth magnetic ramp tones.',
|
description: 'Vast cathedral-scale spatial reverberation, pristine futuristic pulses, and smooth magnetic ramp tones.',
|
||||||
hull: { volume: 0.65, baseFreq: 48, filterCutoff: 95, resonance: 2.0, noiseMix: 0.42, harmonicSpread: 1.01 },
|
hull: { volume: 0.65, baseFreq: 48, filterCutoff: 95, resonance: 2.0, noiseMix: 0.42, harmonicSpread: 1.01 },
|
||||||
warp: { volume: 0.6, bpm: 50, carrierFreq: 60, filterCutoff: 160, pulseShape: 'tng', resonance: 2.8, swirlMix: 0.3 },
|
warp: { volume: 0.6, bpm: 50, carrierFreq: 60, filterCutoff: 160, pulseShape: 'tng', resonance: 2.8, swirlMix: 0.3 },
|
||||||
lifeSupport: { volume: 0.7, noiseType: 'white', highpassFreq: 160, lowpassFreq: 2600, airflowModSpeed: 0.14, airflowModDepth: 0.12 },
|
lifeSupport: { volume: 0.7, noiseType: 'white', highpassFreq: 160, lowpassFreq: 2600, airflowModSpeed: 0.14, airflowModDepth: 0.12 },
|
||||||
telemetry: { volume: 0.45, density: 0.6, era: 'who' }
|
telemetry: { volume: 0.45, density: 0.6, era: 'whataverse' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1071,13 +1071,13 @@ const PresetEngineProfiles = {
|
|||||||
// Starflight NX
|
// Starflight NX
|
||||||
'nx-bridge': 'nx',
|
'nx-bridge': 'nx',
|
||||||
// Whataverse TARDIX
|
// Whataverse TARDIX
|
||||||
'tardis-1963': 'tardis',
|
'tardix-1963': 'tardix',
|
||||||
'tardis-victorian': 'tardis',
|
'tardix-victorian': 'tardix',
|
||||||
'tardis-coral': 'tardis',
|
'tardix-coral': 'tardix',
|
||||||
'tardis-copper': 'tardis',
|
'tardix-copper': 'tardix',
|
||||||
'tardis-cold-machine': 'tardis',
|
'tardix-cold-machine': 'tardix',
|
||||||
'tardis-crystal': 'tardis',
|
'tardix-crystal': 'tardix',
|
||||||
'tardis-infinite-white': 'tardis',
|
'tardix-infinite-white': 'tardix',
|
||||||
// Industrial
|
// Industrial
|
||||||
'ind-nostromo': 'industrial',
|
'ind-nostromo': 'industrial',
|
||||||
'ind-serenity': 'outlaw',
|
'ind-serenity': 'outlaw',
|
||||||
@@ -1147,7 +1147,7 @@ function getEngineProfileForPreset(presetId, universeId) {
|
|||||||
}
|
}
|
||||||
const universeDefaults = {
|
const universeDefaults = {
|
||||||
starfleet: 'galaxy',
|
starfleet: 'galaxy',
|
||||||
whoniverse: 'tardis',
|
whataverse: 'tardix',
|
||||||
industrial: 'industrial',
|
industrial: 'industrial',
|
||||||
bioships: 'bioship',
|
bioships: 'bioship',
|
||||||
retrofuture: 'retrofuture',
|
retrofuture: 'retrofuture',
|
||||||
@@ -1168,4 +1168,3 @@ window.getEngineProfileForPreset = getEngineProfileForPreset;
|
|||||||
* LCARD Audio Visualizer & Worp Core Intermix Chamber Display
|
* LCARD Audio Visualizer & Worp Core Intermix Chamber Display
|
||||||
* Renders real-time audio spectrum, waveform oscilloscope, and pulsing worp core animation.
|
* Renders real-time audio spectrum, waveform oscilloscope, and pulsing worp core animation.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
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;
|
||||||
@@ -143,11 +143,11 @@ window.ObservationBezels = {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (universeId === 'whoniverse') {
|
if (universeId === 'whataverse') {
|
||||||
// Gallifreyan Observatory Roundel Viewing Portal / TARDIX Frame
|
// Gallifreyan Observatory Roundel Viewing Portal / TARDIX Frame
|
||||||
// Console era (classic/revival/modern) tints the rings and label.
|
// Console era (classic/revival/modern) tints the rings and label.
|
||||||
let era = 'revival';
|
let era = 'revival';
|
||||||
const wUniverse = (typeof UniverseRegistry !== 'undefined') ? UniverseRegistry['whoniverse'] : null;
|
const wUniverse = (typeof UniverseRegistry !== 'undefined') ? UniverseRegistry['whataverse'] : null;
|
||||||
const preset = (wUniverse && wUniverse.presets && presetId) ? wUniverse.presets[presetId] : null;
|
const preset = (wUniverse && wUniverse.presets && presetId) ? wUniverse.presets[presetId] : null;
|
||||||
if (preset && preset.era) era = preset.era;
|
if (preset && preset.era) era = preset.era;
|
||||||
|
|
||||||
|
|||||||
+837
-31
@@ -5,7 +5,7 @@ const OBSERVATION_CANVAS = {
|
|||||||
starfield: { count: 150, sizeMin: 0.6, sizeRange: 1.5, warpStreaks: true,
|
starfield: { count: 150, sizeMin: 0.6, sizeRange: 1.5, warpStreaks: true,
|
||||||
colors: ['#e0f2fe', '#ffffff', '#bfdbfe', '#93c5fd', '#a5f3fc'] } },
|
colors: ['#e0f2fe', '#ffffff', '#bfdbfe', '#93c5fd', '#a5f3fc'] } },
|
||||||
|
|
||||||
whoniverse: { canvas: true, layers: { celestial: true } }, // Time Vortex on black, deliberately starless
|
whataverse: { canvas: true, layers: { celestial: true } }, // Time Vortex on black, deliberately starless
|
||||||
|
|
||||||
deepspace: { canvas: true, layers: { starfield: true, deepSpaceField: true,
|
deepspace: { canvas: true, layers: { starfield: true, deepSpaceField: true,
|
||||||
constellations: true, shootingStars: true,
|
constellations: true, shootingStars: true,
|
||||||
@@ -57,6 +57,8 @@ class ObservationEngine {
|
|||||||
this.mainValPillars = document.getElementById('val-observation-pillars');
|
this.mainValPillars = document.getElementById('val-observation-pillars');
|
||||||
this.waveformCanvas = document.getElementById('observation-waveform-canvas');
|
this.waveformCanvas = document.getElementById('observation-waveform-canvas');
|
||||||
this.waveformCtx = this.waveformCanvas ? this.waveformCanvas.getContext('2d') : null;
|
this.waveformCtx = this.waveformCanvas ? this.waveformCanvas.getContext('2d') : null;
|
||||||
|
this.waveformContainer = document.getElementById('observation-waveform-container');
|
||||||
|
this.waveformLabelEl = this.waveformContainer ? this.waveformContainer.querySelector('.observation-waveform-label') : null;
|
||||||
|
|
||||||
// State
|
// State
|
||||||
this.running = false;
|
this.running = false;
|
||||||
@@ -173,7 +175,7 @@ class ObservationEngine {
|
|||||||
|
|
||||||
resize() {
|
resize() {
|
||||||
if (!this.canvas) return;
|
if (!this.canvas) return;
|
||||||
this.dpr = window.devicePixelRatio || 1;
|
this.dpr = (window.devicePixelRatio || 1) * (window.xzbtRenderScale || 1);
|
||||||
this.width = window.innerWidth;
|
this.width = window.innerWidth;
|
||||||
this.height = window.innerHeight;
|
this.height = window.innerHeight;
|
||||||
|
|
||||||
@@ -329,6 +331,7 @@ class ObservationEngine {
|
|||||||
start(universeId) {
|
start(universeId) {
|
||||||
this.activeUniverse = universeId || 'starfleet';
|
this.activeUniverse = universeId || 'starfleet';
|
||||||
this.running = true;
|
this.running = true;
|
||||||
|
this.applyWaveformTheme();
|
||||||
|
|
||||||
// --- v3co: apply the OBSERVATION_CANVAS manifest for this universe -------------------
|
// --- v3co: apply the OBSERVATION_CANVAS manifest for this universe -------------------
|
||||||
// Barrier 2 of 3: the overlay class scopes the background-rect CSS rule; the canvas
|
// Barrier 2 of 3: the overlay class scopes the background-rect CSS rule; the canvas
|
||||||
@@ -645,8 +648,8 @@ class ObservationEngine {
|
|||||||
label = `RUNABOUT YANGTZE // NCC-72452`;
|
label = `RUNABOUT YANGTZE // NCC-72452`;
|
||||||
trailColor = '#f97316';
|
trailColor = '#f97316';
|
||||||
}
|
}
|
||||||
} else if (this.activeUniverse === 'whoniverse') {
|
} else if (this.activeUniverse === 'whataverse') {
|
||||||
shipType = 'tardis';
|
shipType = 'tardix';
|
||||||
label = 'TYPE 4D TIME CAPSULE // DRIFT';
|
label = 'TYPE 4D TIME CAPSULE // DRIFT';
|
||||||
trailColor = '#00e5ff';
|
trailColor = '#00e5ff';
|
||||||
} else if (this.activeUniverse === 'industrial' || this.activeUniverse === 'outlaw') {
|
} else if (this.activeUniverse === 'industrial' || this.activeUniverse === 'outlaw') {
|
||||||
@@ -822,7 +825,7 @@ class ObservationEngine {
|
|||||||
getNebulaPalette() {
|
getNebulaPalette() {
|
||||||
let g1 = '#3b82f6', g2 = '#6366f1', g3 = '#ec4899';
|
let g1 = '#3b82f6', g2 = '#6366f1', g3 = '#ec4899';
|
||||||
|
|
||||||
if (this.activeUniverse === 'whoniverse') {
|
if (this.activeUniverse === 'whataverse') {
|
||||||
g1 = '#00e5ff'; g2 = '#d4af37'; g3 = '#8b5cf6';
|
g1 = '#00e5ff'; g2 = '#d4af37'; g3 = '#8b5cf6';
|
||||||
} else if (this.activeUniverse === 'industrial') {
|
} else if (this.activeUniverse === 'industrial') {
|
||||||
g1 = '#f59e0b'; g2 = '#b45309'; g3 = '#3f2c18';
|
g1 = '#f59e0b'; g2 = '#b45309'; g3 = '#3f2c18';
|
||||||
@@ -891,7 +894,7 @@ class ObservationEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderCelestialObjects(ctx, cx, cy) {
|
renderCelestialObjects(ctx, cx, cy) {
|
||||||
if (this.activeUniverse === 'whoniverse') {
|
if (this.activeUniverse === 'whataverse') {
|
||||||
// Procedural 3D Time Vortex
|
// Procedural 3D Time Vortex
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.translate(cx, cy);
|
ctx.translate(cx, cy);
|
||||||
@@ -917,10 +920,41 @@ class ObservationEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.activeUniverse === 'starfleet') {
|
if (this.activeUniverse === 'starfleet') {
|
||||||
// Class-M Planet with Rayleigh Scattering Atmosphere Halo
|
// Class-M Planet with properly occluded moon orbit: the rear half of the ellipse is
|
||||||
|
// drawn behind the planet and the forward half is drawn over it, so the moon actually
|
||||||
|
// passes behind the world rather than remaining glued to the viewer side.
|
||||||
const px = cx + this.width * 0.26;
|
const px = cx + this.width * 0.26;
|
||||||
const py = cy - this.height * 0.06;
|
const py = cy - this.height * 0.06;
|
||||||
const pr = Math.min(this.width, this.height) * 0.22;
|
const pr = Math.min(this.width, this.height) * 0.22;
|
||||||
|
const orbitRx = pr * 1.7;
|
||||||
|
const orbitRy = pr * 0.6;
|
||||||
|
const moonAngle = this.time * 0.08;
|
||||||
|
const moonFront = Math.sin(moonAngle) > 0;
|
||||||
|
const mx = px + Math.cos(moonAngle) * orbitRx;
|
||||||
|
const my = py + Math.sin(moonAngle) * orbitRy;
|
||||||
|
const mr = pr * 0.14;
|
||||||
|
|
||||||
|
const drawMoon = () => {
|
||||||
|
ctx.fillStyle = '#cbd5e1';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(mx, my, mr, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.fillStyle = 'rgba(2, 6, 23, 0.75)';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(mx + mr * 0.3, my, mr * 0.95, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Rear orbit arc and rear moon first.
|
||||||
|
ctx.save();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.ellipse(px, py, orbitRx, orbitRy, 0, Math.PI, Math.PI * 2);
|
||||||
|
ctx.strokeStyle = 'rgba(186, 230, 253, 0.18)';
|
||||||
|
ctx.lineWidth = 1.6;
|
||||||
|
ctx.setLineDash([8, 10]);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
if (!moonFront) drawMoon();
|
||||||
|
|
||||||
// Outer Atmospheric Glow
|
// Outer Atmospheric Glow
|
||||||
const atmosGrad = ctx.createRadialGradient(px, py, pr * 0.85, px, py, pr * 1.35);
|
const atmosGrad = ctx.createRadialGradient(px, py, pr * 0.85, px, py, pr * 1.35);
|
||||||
@@ -938,7 +972,6 @@ class ObservationEngine {
|
|||||||
bodyGrad.addColorStop(0.4, '#1d4ed8');
|
bodyGrad.addColorStop(0.4, '#1d4ed8');
|
||||||
bodyGrad.addColorStop(0.7, '#0f172a');
|
bodyGrad.addColorStop(0.7, '#0f172a');
|
||||||
bodyGrad.addColorStop(1, '#020617');
|
bodyGrad.addColorStop(1, '#020617');
|
||||||
|
|
||||||
ctx.fillStyle = bodyGrad;
|
ctx.fillStyle = bodyGrad;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(px, py, pr, 0, Math.PI * 2);
|
ctx.arc(px, py, pr, 0, Math.PI * 2);
|
||||||
@@ -951,22 +984,16 @@ class ObservationEngine {
|
|||||||
ctx.lineWidth = 3.5;
|
ctx.lineWidth = 3.5;
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// Orbiting Moon
|
// Forward orbit arc and front moon last.
|
||||||
const moonAngle = this.time * 0.08;
|
ctx.save();
|
||||||
const mx = px + Math.cos(moonAngle) * (pr * 1.7);
|
|
||||||
const my = py + Math.sin(moonAngle) * (pr * 0.6);
|
|
||||||
const mr = pr * 0.14;
|
|
||||||
|
|
||||||
ctx.fillStyle = '#cbd5e1';
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(mx, my, mr, 0, Math.PI * 2);
|
ctx.ellipse(px, py, orbitRx, orbitRy, 0, 0, Math.PI);
|
||||||
ctx.fill();
|
ctx.strokeStyle = 'rgba(186, 230, 253, 0.34)';
|
||||||
|
ctx.lineWidth = 1.8;
|
||||||
// Moon shadow
|
ctx.setLineDash([10, 8]);
|
||||||
ctx.fillStyle = 'rgba(2, 6, 23, 0.75)';
|
ctx.stroke();
|
||||||
ctx.beginPath();
|
ctx.restore();
|
||||||
ctx.arc(mx + mr * 0.3, my, mr * 0.95, 0, Math.PI * 2);
|
if (moonFront) drawMoon();
|
||||||
ctx.fill();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1615,7 +1642,7 @@ class ObservationEngine {
|
|||||||
ctx.arc(-2, -19.5, 2.5, 0, Math.PI * 2);
|
ctx.arc(-2, -19.5, 2.5, 0, Math.PI * 2);
|
||||||
ctx.arc(-2, 19.5, 2.5, 0, Math.PI * 2);
|
ctx.arc(-2, 19.5, 2.5, 0, Math.PI * 2);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
} else if (ship.type === 'tardis') {
|
} else if (ship.type === 'tardix') {
|
||||||
// Whataverse TARDIX tumbling in vortex
|
// Whataverse TARDIX tumbling in vortex
|
||||||
ctx.rotate(this.time * 1.5);
|
ctx.rotate(this.time * 1.5);
|
||||||
ctx.fillStyle = '#1e3a8a';
|
ctx.fillStyle = '#1e3a8a';
|
||||||
@@ -1771,11 +1798,458 @@ class ObservationEngine {
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getWaveformThemeConfig() {
|
||||||
|
const defaults = {
|
||||||
|
key: 'starflight',
|
||||||
|
style: 'starflight',
|
||||||
|
label: 'SUBSPACE HARMONICS',
|
||||||
|
labelLetterSpacing: '0.2em',
|
||||||
|
labelSize: '0.65rem',
|
||||||
|
panelOpacity: '0.84',
|
||||||
|
position: {
|
||||||
|
top: '',
|
||||||
|
right: '',
|
||||||
|
bottom: '2.2vh',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
alignItems: 'center',
|
||||||
|
width: 'clamp(260px, 34vw, 460px)'
|
||||||
|
},
|
||||||
|
canvas: {
|
||||||
|
width: '100%',
|
||||||
|
height: '38px',
|
||||||
|
radius: '8px',
|
||||||
|
background: 'transparent'
|
||||||
|
},
|
||||||
|
panel: {
|
||||||
|
padding: '8px 14px',
|
||||||
|
borderRadius: '14px',
|
||||||
|
background: 'linear-gradient(180deg, rgba(2,12,28,0.28), rgba(1,5,16,0.1))',
|
||||||
|
border: '1px solid rgba(103,232,249,0.18)',
|
||||||
|
boxShadow: '0 0 18px rgba(56,189,248,0.1), inset 0 0 18px rgba(8,47,73,0.12)',
|
||||||
|
backdropFilter: 'blur(2px)'
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
primary: '#67e8f9',
|
||||||
|
secondary: '#60a5fa',
|
||||||
|
tertiary: '#dbeafe',
|
||||||
|
accent: '#f59e0b',
|
||||||
|
panel: 'rgba(3, 10, 24, 0.25)'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (this.activeUniverse) {
|
||||||
|
case 'whataverse':
|
||||||
|
return {
|
||||||
|
key: 'whataverse',
|
||||||
|
style: 'whataverseNodes',
|
||||||
|
label: 'TEMPORAL NODE FIELD',
|
||||||
|
panelOpacity: '0.9',
|
||||||
|
position: {
|
||||||
|
top: '8vh',
|
||||||
|
right: '',
|
||||||
|
bottom: '',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
alignItems: 'center',
|
||||||
|
width: 'clamp(340px, 42vw, 580px)'
|
||||||
|
},
|
||||||
|
canvas: { width: '100%', height: '82px', radius: '16px', background: 'transparent' },
|
||||||
|
panel: {
|
||||||
|
padding: '10px 18px 12px',
|
||||||
|
borderRadius: '18px',
|
||||||
|
background: 'radial-gradient(circle at 50% 50%, rgba(8,24,48,0.2), rgba(2,8,20,0.08))',
|
||||||
|
border: '1px solid rgba(0,229,255,0.25)',
|
||||||
|
boxShadow: '0 0 28px rgba(0,229,255,0.12), inset 0 0 28px rgba(212,175,55,0.07)',
|
||||||
|
backdropFilter: 'blur(1px)'
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
primary: '#65f3ff',
|
||||||
|
secondary: '#3b82f6',
|
||||||
|
tertiary: '#d4af37',
|
||||||
|
accent: '#f8fafc',
|
||||||
|
panel: 'rgba(6,18,34,0.18)'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
case 'industrial':
|
||||||
|
return {
|
||||||
|
key: 'industrial',
|
||||||
|
style: 'industrialScope',
|
||||||
|
label: 'DRIVE VIBRATION SPECTRUM',
|
||||||
|
position: {
|
||||||
|
top: 'auto',
|
||||||
|
right: 'auto',
|
||||||
|
bottom: '12.5vh',
|
||||||
|
left: '3vw',
|
||||||
|
transform: 'none',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
width: 'clamp(240px, 24vw, 360px)'
|
||||||
|
},
|
||||||
|
canvas: { width: '100%', height: '58px', radius: '8px', background: 'rgba(8,7,4,0.35)' },
|
||||||
|
panel: {
|
||||||
|
padding: '8px 10px 10px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
background: 'linear-gradient(180deg, rgba(28,18,8,0.25), rgba(8,5,2,0.15))',
|
||||||
|
border: '1px solid rgba(245,158,11,0.28)',
|
||||||
|
boxShadow: '0 0 18px rgba(245,158,11,0.11), inset 0 0 16px rgba(120,53,15,0.16)',
|
||||||
|
backdropFilter: 'blur(1px)'
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
primary: '#f59e0b',
|
||||||
|
secondary: '#f97316',
|
||||||
|
tertiary: '#fcd34d',
|
||||||
|
accent: '#f8fafc',
|
||||||
|
panel: 'rgba(20,10,3,0.35)'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
case 'bioships':
|
||||||
|
return {
|
||||||
|
key: 'bioships',
|
||||||
|
style: 'bioshipMembrane',
|
||||||
|
label: 'NEURAL RESONANCE',
|
||||||
|
position: {
|
||||||
|
top: '16vh',
|
||||||
|
right: '1.8vw',
|
||||||
|
bottom: 'auto',
|
||||||
|
left: 'auto',
|
||||||
|
transform: 'none',
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
width: 'clamp(250px, 24vw, 340px)'
|
||||||
|
},
|
||||||
|
canvas: { width: '100%', height: '84px', radius: '20px', background: 'transparent' },
|
||||||
|
panel: {
|
||||||
|
padding: '8px 14px 12px',
|
||||||
|
borderRadius: '22px',
|
||||||
|
background:
|
||||||
|
'radial-gradient(circle at 30% 50%, rgba(16,185,129,0.18), rgba(1,20,14,0.08))',
|
||||||
|
border: '1px solid rgba(16,185,129,0.24)',
|
||||||
|
boxShadow: '0 0 22px rgba(16,185,129,0.1), inset 0 0 28px rgba(139,92,246,0.08)',
|
||||||
|
backdropFilter: 'blur(1px)'
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
primary: '#34d399',
|
||||||
|
secondary: '#10b981',
|
||||||
|
tertiary: '#a78bfa',
|
||||||
|
accent: '#ecfeff',
|
||||||
|
panel: 'rgba(1,24,18,0.22)'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
case 'retrofuture':
|
||||||
|
return {
|
||||||
|
key: 'retrofuture',
|
||||||
|
style: 'retroCrt',
|
||||||
|
label: 'HARMONIC TRACE',
|
||||||
|
position: {
|
||||||
|
top: '',
|
||||||
|
right: '',
|
||||||
|
bottom: '4.2vh',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
alignItems: 'center',
|
||||||
|
width: 'clamp(260px, 34vw, 440px)'
|
||||||
|
},
|
||||||
|
canvas: { width: '100%', height: '56px', radius: '6px', background: 'rgba(0,18,4,0.45)' },
|
||||||
|
panel: {
|
||||||
|
padding: '10px 14px 12px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
background: 'linear-gradient(180deg, rgba(2,14,4,0.22), rgba(1,10,3,0.14))',
|
||||||
|
border: '1px solid rgba(34,197,94,0.28)',
|
||||||
|
boxShadow: '0 0 18px rgba(34,197,94,0.1), inset 0 0 20px rgba(20,83,45,0.14)',
|
||||||
|
backdropFilter: 'none'
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
primary: '#4ade80',
|
||||||
|
secondary: '#22c55e',
|
||||||
|
tertiary: '#dcfce7',
|
||||||
|
accent: '#86efac',
|
||||||
|
panel: 'rgba(0,20,4,0.32)'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
case 'military':
|
||||||
|
return {
|
||||||
|
key: 'military',
|
||||||
|
style: 'militaryBlocks',
|
||||||
|
label: 'TACTICAL CARRIER ANALYSIS',
|
||||||
|
labelLetterSpacing: '0.16em',
|
||||||
|
position: {
|
||||||
|
top: '34vh',
|
||||||
|
right: '2.8vw',
|
||||||
|
bottom: 'auto',
|
||||||
|
left: 'auto',
|
||||||
|
transform: 'none',
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
width: 'clamp(240px, 22vw, 340px)'
|
||||||
|
},
|
||||||
|
canvas: {
|
||||||
|
width: '100%',
|
||||||
|
height: '82px',
|
||||||
|
radius: '8px',
|
||||||
|
background: 'rgba(10,10,12,0.3)'
|
||||||
|
},
|
||||||
|
panel: {
|
||||||
|
padding: '8px 10px 10px',
|
||||||
|
borderRadius: '10px',
|
||||||
|
background: 'linear-gradient(180deg, rgba(10,10,12,0.2), rgba(8,8,10,0.15))',
|
||||||
|
border: '1px solid rgba(148,163,184,0.26)',
|
||||||
|
boxShadow: '0 0 18px rgba(100,116,139,0.1), inset 0 0 16px rgba(127,29,29,0.1)',
|
||||||
|
backdropFilter: 'blur(1px)'
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
primary: '#cbd5e1',
|
||||||
|
secondary: '#64748b',
|
||||||
|
tertiary: '#ef4444',
|
||||||
|
accent: '#f8fafc',
|
||||||
|
panel: 'rgba(10,12,18,0.35)'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
case 'deepspace':
|
||||||
|
return {
|
||||||
|
key: 'deepspace',
|
||||||
|
style: 'deepSpaceField',
|
||||||
|
label: 'FIELD INTERFERENCE',
|
||||||
|
position: {
|
||||||
|
top: '11vh',
|
||||||
|
right: 'auto',
|
||||||
|
bottom: 'auto',
|
||||||
|
left: '6vw',
|
||||||
|
transform: 'none',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
width: 'clamp(280px, 30vw, 460px)'
|
||||||
|
},
|
||||||
|
canvas: { width: '100%', height: '72px', radius: '10px', background: 'transparent' },
|
||||||
|
panel: {
|
||||||
|
padding: '8px 12px 10px',
|
||||||
|
borderRadius: '14px',
|
||||||
|
background: 'linear-gradient(180deg, rgba(4,8,22,0.24), rgba(1,4,12,0.08))',
|
||||||
|
border: '1px solid rgba(56,189,248,0.18)',
|
||||||
|
boxShadow: '0 0 18px rgba(59,130,246,0.08), inset 0 0 18px rgba(245,158,11,0.06)',
|
||||||
|
backdropFilter: 'blur(1px)'
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
primary: '#93c5fd',
|
||||||
|
secondary: '#60a5fa',
|
||||||
|
tertiary: '#f59e0b',
|
||||||
|
accent: '#f8fafc',
|
||||||
|
panel: 'rgba(2,8,20,0.15)'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
case 'outlaw':
|
||||||
|
return {
|
||||||
|
key: 'outlaw',
|
||||||
|
style: 'outlawGauges',
|
||||||
|
label: 'FIELD TUNING',
|
||||||
|
position: {
|
||||||
|
top: '',
|
||||||
|
right: '',
|
||||||
|
bottom: '7vh',
|
||||||
|
left: '4vw',
|
||||||
|
transform: 'none',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
width: 'clamp(290px, 26vw, 400px)'
|
||||||
|
},
|
||||||
|
canvas: {
|
||||||
|
width: '100%',
|
||||||
|
height: '126px',
|
||||||
|
radius: '10px',
|
||||||
|
background: 'rgba(18,8,10,0.25)'
|
||||||
|
},
|
||||||
|
panel: {
|
||||||
|
padding: '8px 12px 10px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
background: 'linear-gradient(180deg, rgba(28,8,12,0.22), rgba(10,4,8,0.12))',
|
||||||
|
border: '1px solid rgba(249,115,22,0.25)',
|
||||||
|
boxShadow: '0 0 18px rgba(249,115,22,0.12), inset 0 0 16px rgba(123,61,184,0.09)',
|
||||||
|
backdropFilter: 'blur(1px)'
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
primary: '#fb923c',
|
||||||
|
secondary: '#a855f7',
|
||||||
|
tertiary: '#fde68a',
|
||||||
|
accent: '#f8fafc',
|
||||||
|
panel: 'rgba(18,8,10,0.24)'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
case 'spacestations':
|
||||||
|
return {
|
||||||
|
key: 'spacestations',
|
||||||
|
style: 'stationMatrix',
|
||||||
|
label: 'STATION RESONANCE MATRIX',
|
||||||
|
labelLetterSpacing: '0.16em',
|
||||||
|
position: {
|
||||||
|
top: '18vh',
|
||||||
|
right: '2.3vw',
|
||||||
|
bottom: 'auto',
|
||||||
|
left: 'auto',
|
||||||
|
transform: 'none',
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
width: 'clamp(280px, 24vw, 360px)'
|
||||||
|
},
|
||||||
|
canvas: {
|
||||||
|
width: '100%',
|
||||||
|
height: '136px',
|
||||||
|
radius: '10px',
|
||||||
|
background: 'rgba(3,12,22,0.24)'
|
||||||
|
},
|
||||||
|
panel: {
|
||||||
|
padding: '8px 12px 10px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
background: 'linear-gradient(180deg, rgba(2,12,24,0.22), rgba(2,8,15,0.14))',
|
||||||
|
border: '1px solid rgba(56,189,248,0.24)',
|
||||||
|
boxShadow: '0 0 18px rgba(56,189,248,0.1), inset 0 0 18px rgba(14,165,233,0.08)',
|
||||||
|
backdropFilter: 'blur(1px)'
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
primary: '#38bdf8',
|
||||||
|
secondary: '#22d3ee',
|
||||||
|
tertiary: '#f59e0b',
|
||||||
|
accent: '#f8fafc',
|
||||||
|
panel: 'rgba(4,14,24,0.24)'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
case 'comedy':
|
||||||
|
return {
|
||||||
|
key: 'comedy',
|
||||||
|
style: 'comedyAdventure',
|
||||||
|
label: 'DRIVE HARMONICS',
|
||||||
|
position: {
|
||||||
|
top: '',
|
||||||
|
right: '4vw',
|
||||||
|
bottom: '6vh',
|
||||||
|
left: '',
|
||||||
|
transform: 'none',
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
width: 'clamp(280px, 30vw, 440px)'
|
||||||
|
},
|
||||||
|
canvas: { width: '100%', height: '62px', radius: '12px', background: 'transparent' },
|
||||||
|
panel: {
|
||||||
|
padding: '8px 12px 10px',
|
||||||
|
borderRadius: '16px',
|
||||||
|
background: 'linear-gradient(180deg, rgba(28,16,52,0.2), rgba(10,8,24,0.1))',
|
||||||
|
border: '1px solid rgba(244,114,182,0.22)',
|
||||||
|
boxShadow: '0 0 20px rgba(167,139,250,0.12), inset 0 0 18px rgba(253,224,71,0.07)',
|
||||||
|
backdropFilter: 'blur(1px)'
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
primary: '#f472b6',
|
||||||
|
secondary: '#a78bfa',
|
||||||
|
tertiary: '#fde047',
|
||||||
|
accent: '#f8fafc',
|
||||||
|
panel: 'rgba(16,10,34,0.18)'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return defaults;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyWaveformTheme() {
|
||||||
|
const cfg = this.getWaveformThemeConfig();
|
||||||
|
if (this.waveformContainer) {
|
||||||
|
const s = this.waveformContainer.style;
|
||||||
|
s.top = cfg.position.top ?? 'auto';
|
||||||
|
s.right = cfg.position.right ?? 'auto';
|
||||||
|
s.bottom = cfg.position.bottom ?? 'auto';
|
||||||
|
s.left = cfg.position.left ?? 'auto';
|
||||||
|
s.transform = cfg.position.transform || 'none';
|
||||||
|
s.alignItems = cfg.position.alignItems || 'center';
|
||||||
|
s.width = cfg.position.width || 'clamp(260px, 34vw, 460px)';
|
||||||
|
s.padding = cfg.panel.padding || '8px 12px';
|
||||||
|
s.borderRadius = cfg.panel.borderRadius || '14px';
|
||||||
|
s.background = cfg.panel.background || 'transparent';
|
||||||
|
s.border = cfg.panel.border || 'none';
|
||||||
|
s.boxShadow = cfg.panel.boxShadow || 'none';
|
||||||
|
s.backdropFilter = cfg.panel.backdropFilter || 'none';
|
||||||
|
s.opacity = cfg.panelOpacity || '0.84';
|
||||||
|
}
|
||||||
|
if (this.waveformLabelEl) {
|
||||||
|
this.waveformLabelEl.textContent = cfg.label || 'SUBSPACE HARMONICS';
|
||||||
|
this.waveformLabelEl.style.color = cfg.colors.tertiary || cfg.colors.primary || '#fff';
|
||||||
|
this.waveformLabelEl.style.letterSpacing = cfg.labelLetterSpacing || '0.2em';
|
||||||
|
this.waveformLabelEl.style.fontSize = cfg.labelSize || '0.65rem';
|
||||||
|
this.waveformLabelEl.style.width = '100%';
|
||||||
|
this.waveformLabelEl.style.textAlign =
|
||||||
|
cfg.position.alignItems === 'center'
|
||||||
|
? 'center'
|
||||||
|
: cfg.position.alignItems === 'flex-end'
|
||||||
|
? 'right'
|
||||||
|
: 'left';
|
||||||
|
}
|
||||||
|
if (this.waveformCanvas) {
|
||||||
|
this.waveformCanvas.style.width = cfg.canvas.width || '100%';
|
||||||
|
this.waveformCanvas.style.height = cfg.canvas.height || '38px';
|
||||||
|
this.waveformCanvas.style.borderRadius = cfg.canvas.radius || '8px';
|
||||||
|
this.waveformCanvas.style.background = cfg.canvas.background || 'transparent';
|
||||||
|
this.waveformCanvas.style.display = 'block';
|
||||||
|
this.waveformCanvas.style.boxShadow = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getWaveformEnergySummary() {
|
||||||
|
const bandAvg = (start, end) => {
|
||||||
|
let total = 0;
|
||||||
|
let count = 0;
|
||||||
|
for (let i = start; i <= end && i < this.analyserData.length; i++) {
|
||||||
|
total += this.analyserData[i] || 0;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
return count ? total / count / 255 : 0;
|
||||||
|
};
|
||||||
|
const bass = bandAvg(0, 10);
|
||||||
|
const lowMid = bandAvg(11, 22);
|
||||||
|
const mid = bandAvg(23, 38);
|
||||||
|
const treble = bandAvg(39, 63);
|
||||||
|
const overall = (bass + lowMid + mid + treble) / 4;
|
||||||
|
return { bass, lowMid, mid, treble, overall };
|
||||||
|
}
|
||||||
|
|
||||||
|
drawWaveformGrid(ctx, w, h, color, cols, rows, alpha) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = color;
|
||||||
|
ctx.globalAlpha = alpha;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
for (let i = 1; i < cols; i++) {
|
||||||
|
const x = (w * i) / cols;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, 0);
|
||||||
|
ctx.lineTo(x, h);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
for (let i = 1; i < rows; i++) {
|
||||||
|
const y = (h * i) / rows;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, y);
|
||||||
|
ctx.lineTo(w, y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
drawSmoothPath(ctx, points, color, width, alpha) {
|
||||||
|
if (!points || points.length < 2) return;
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = color;
|
||||||
|
ctx.globalAlpha = alpha;
|
||||||
|
ctx.lineWidth = width;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(points[0].x, points[0].y);
|
||||||
|
for (let i = 1; i < points.length - 1; i++) {
|
||||||
|
const xc = (points[i].x + points[i + 1].x) / 2;
|
||||||
|
const yc = (points[i].y + points[i + 1].y) / 2;
|
||||||
|
ctx.quadraticCurveTo(points[i].x, points[i].y, xc, yc);
|
||||||
|
}
|
||||||
|
const last = points[points.length - 1];
|
||||||
|
ctx.lineTo(last.x, last.y);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
renderWaveform() {
|
renderWaveform() {
|
||||||
if (!this.waveformCtx || !this.waveformCanvas) return;
|
if (!this.waveformCtx || !this.waveformCanvas) return;
|
||||||
const ctx = this.waveformCtx;
|
const ctx = this.waveformCtx;
|
||||||
const w = this.waveformCanvas.width / (this.dpr || 1);
|
const w = this.waveformCanvas.width / (this.dpr || 1);
|
||||||
const h = this.waveformCanvas.height / (this.dpr || 1);
|
const h = this.waveformCanvas.height / (this.dpr || 1);
|
||||||
|
const cfg = this.getWaveformThemeConfig();
|
||||||
|
|
||||||
ctx.clearRect(0, 0, w, h);
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
|
||||||
@@ -1783,11 +2257,344 @@ class ObservationEngine {
|
|||||||
this.am.analyser.getByteFrequencyData(this.analyserData);
|
this.am.analyser.getByteFrequencyData(this.analyserData);
|
||||||
}
|
}
|
||||||
|
|
||||||
const bars = 30;
|
const e = this.getWaveformEnergySummary();
|
||||||
const barW = Math.max(3, (w / bars) - 2);
|
const t = this.time || 0;
|
||||||
const accent = getComputedStyle(document.body).getPropertyValue('--primary-accent').trim() || '#ff9900';
|
const p = cfg.colors.primary;
|
||||||
|
const s = cfg.colors.secondary;
|
||||||
|
const a = cfg.colors.tertiary;
|
||||||
|
const baseline = h * 0.5;
|
||||||
|
|
||||||
ctx.fillStyle = accent;
|
if (cfg.style === 'whataverseNodes') {
|
||||||
|
const nodes = [];
|
||||||
|
const count = 15;
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const x = (w * i) / (count - 1);
|
||||||
|
const phase = t * 1.35 + i * 0.55;
|
||||||
|
const drift = Math.sin(phase) * (h * 0.12) + Math.cos(phase * 0.47 + i) * (h * 0.05);
|
||||||
|
const y = baseline + drift + Math.sin(t * 0.5 + i * 1.1) * (e.mid * h * 0.16);
|
||||||
|
nodes.push({ x, y });
|
||||||
|
}
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = s;
|
||||||
|
ctx.globalAlpha = 0.2;
|
||||||
|
for (let i = 0; i < nodes.length - 1; i++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(nodes[i].x, nodes[i].y + 8 * Math.sin(t + i));
|
||||||
|
ctx.lineTo(nodes[i + 1].x, nodes[i + 1].y + 8 * Math.cos(t * 1.1 + i));
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
this.drawSmoothPath(ctx, nodes, p, 2.4, 0.95);
|
||||||
|
const echo = nodes.map((pt, i) => ({
|
||||||
|
x: pt.x,
|
||||||
|
y: pt.y + Math.sin(t * 1.8 + i * 0.6) * 10 + 10
|
||||||
|
}));
|
||||||
|
this.drawSmoothPath(ctx, echo, s, 1.4, 0.45);
|
||||||
|
nodes.forEach((pt, i) => {
|
||||||
|
const r = 3 + (i % 4 === 0 ? 2 : 0) + e.treble * 1.5;
|
||||||
|
ctx.fillStyle = i % 3 === 0 ? a : p;
|
||||||
|
ctx.globalAlpha = 0.95;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(pt.x, pt.y, r, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.strokeStyle = i % 3 === 0 ? p : a;
|
||||||
|
ctx.globalAlpha = 0.35;
|
||||||
|
ctx.stroke();
|
||||||
|
});
|
||||||
|
ctx.globalAlpha = 0.16;
|
||||||
|
ctx.strokeStyle = a;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, baseline);
|
||||||
|
ctx.lineTo(w, baseline);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg.style === 'outlawGauges') {
|
||||||
|
const valueA = Math.min(1, 0.25 + e.lowMid * 0.8 + 0.2 * Math.sin(t * 0.8));
|
||||||
|
const valueB = Math.min(1, 0.2 + e.treble * 0.75 + 0.18 * Math.cos(t * 1.1));
|
||||||
|
const valueC = Math.min(1, 0.18 + e.bass * 0.7 + 0.14 * Math.sin(t * 0.6 + 1.2));
|
||||||
|
const gauges = [
|
||||||
|
{ cx: w * 0.28, cy: h * 0.48, r: h * 0.28, value: valueA, color: p, label: 'BUS A' },
|
||||||
|
{ cx: w * 0.72, cy: h * 0.48, r: h * 0.24, value: valueB, color: s, label: 'BUS B' }
|
||||||
|
];
|
||||||
|
ctx.font = '10px Share Tech Mono, monospace';
|
||||||
|
gauges.forEach(g => {
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = 'rgba(255,255,255,0.12)';
|
||||||
|
ctx.lineWidth = 6;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(g.cx, g.cy, g.r, Math.PI * 0.8, Math.PI * 2.2);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.strokeStyle = g.color;
|
||||||
|
ctx.lineWidth = 4;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(g.cx, g.cy, g.r, Math.PI * 0.8, Math.PI * (0.8 + 1.4 * g.value));
|
||||||
|
ctx.stroke();
|
||||||
|
for (let i = 0; i <= 6; i++) {
|
||||||
|
const ang = Math.PI * (0.8 + (1.4 * i) / 6);
|
||||||
|
const r0 = g.r - 8,
|
||||||
|
r1 = g.r + 6;
|
||||||
|
ctx.strokeStyle = 'rgba(255,255,255,0.25)';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(g.cx + Math.cos(ang) * r0, g.cy + Math.sin(ang) * r0);
|
||||||
|
ctx.lineTo(g.cx + Math.cos(ang) * r1, g.cy + Math.sin(ang) * r1);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
const needleAng = Math.PI * (0.8 + 1.4 * g.value);
|
||||||
|
ctx.strokeStyle = a;
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(g.cx, g.cy);
|
||||||
|
ctx.lineTo(
|
||||||
|
g.cx + Math.cos(needleAng) * (g.r - 10),
|
||||||
|
g.cy + Math.sin(needleAng) * (g.r - 10)
|
||||||
|
);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.fillStyle = g.color;
|
||||||
|
ctx.globalAlpha = 0.9;
|
||||||
|
ctx.fillText(g.label, g.cx - 18, g.cy + g.r + 18);
|
||||||
|
ctx.restore();
|
||||||
|
});
|
||||||
|
const traceY = h * 0.88;
|
||||||
|
ctx.strokeStyle = 'rgba(255,255,255,0.18)';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(12, traceY);
|
||||||
|
ctx.lineTo(w - 12, traceY);
|
||||||
|
ctx.stroke();
|
||||||
|
const points = [];
|
||||||
|
for (let i = 0; i < 24; i++) {
|
||||||
|
const x = 12 + (w - 24) * (i / 23);
|
||||||
|
const y =
|
||||||
|
traceY -
|
||||||
|
(Math.sin(t * 2.4 + i * 0.7) * 5 + Math.cos(t * 1.1 + i * 1.5) * 3 + valueC * 10);
|
||||||
|
points.push({ x, y });
|
||||||
|
}
|
||||||
|
this.drawSmoothPath(ctx, points, p, 2, 0.9);
|
||||||
|
ctx.fillStyle = a;
|
||||||
|
ctx.font = '9px Share Tech Mono, monospace';
|
||||||
|
ctx.fillText('TRACE', 12, traceY - 10);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg.style === 'stationMatrix') {
|
||||||
|
this.drawWaveformGrid(ctx, w, h, p, 6, 5, 0.08);
|
||||||
|
const rows = [
|
||||||
|
{ y: h * 0.2, color: p, amp: 6 + e.mid * 10, label: 'TRAF' },
|
||||||
|
{ y: h * 0.4, color: s, amp: 4 + e.lowMid * 8, label: 'PWR' },
|
||||||
|
{ y: h * 0.6, color: a, amp: 5 + e.treble * 9, label: 'DOCK' },
|
||||||
|
{ y: h * 0.8, color: '#cbd5e1', amp: 3 + e.bass * 8, label: 'STRC' }
|
||||||
|
];
|
||||||
|
ctx.font = '10px Share Tech Mono, monospace';
|
||||||
|
rows.forEach((row, idx) => {
|
||||||
|
ctx.fillStyle = row.color;
|
||||||
|
ctx.globalAlpha = 0.75;
|
||||||
|
ctx.fillText(row.label, 6, row.y + 3);
|
||||||
|
const points = [];
|
||||||
|
for (let i = 0; i < 22; i++) {
|
||||||
|
const x = 42 + (w - 50) * (i / 21);
|
||||||
|
const y =
|
||||||
|
row.y +
|
||||||
|
Math.sin(t * (1.2 + idx * 0.25) + i * 0.7) * row.amp +
|
||||||
|
Math.cos(t * 0.8 + i * 1.5) * (row.amp * 0.25);
|
||||||
|
points.push({ x, y });
|
||||||
|
}
|
||||||
|
this.drawSmoothPath(ctx, points, row.color, 1.8, 0.92);
|
||||||
|
});
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg.style === 'militaryBlocks') {
|
||||||
|
this.drawWaveformGrid(ctx, w, h, s, 8, 4, 0.07);
|
||||||
|
const segs = 18;
|
||||||
|
const bw = (w - 24) / segs;
|
||||||
|
for (let i = 0; i < segs; i++) {
|
||||||
|
const v1 = (this.analyserData[i * 2] || 0) / 255;
|
||||||
|
const v2 = (this.analyserData[i * 2 + 1] || 0) / 255;
|
||||||
|
const x = 12 + i * bw;
|
||||||
|
const h1 = 10 + v1 * (h * 0.28);
|
||||||
|
const h2 = 8 + v2 * (h * 0.2);
|
||||||
|
ctx.fillStyle = p;
|
||||||
|
ctx.globalAlpha = 0.82;
|
||||||
|
ctx.fillRect(x, h * 0.42 - h1, bw * 0.64, h1);
|
||||||
|
ctx.fillStyle = s;
|
||||||
|
ctx.globalAlpha = 0.65;
|
||||||
|
ctx.fillRect(x, h * 0.84 - h2, bw * 0.64, h2);
|
||||||
|
}
|
||||||
|
ctx.globalAlpha = 0.75;
|
||||||
|
ctx.fillStyle = a;
|
||||||
|
ctx.font = '10px Share Tech Mono, monospace';
|
||||||
|
ctx.fillText('CARRIER', 10, 12);
|
||||||
|
ctx.fillText('THREAT', 10, h * 0.5 + 6);
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg.style === 'industrialScope') {
|
||||||
|
this.drawWaveformGrid(ctx, w, h, p, 10, 4, 0.08);
|
||||||
|
const points = [];
|
||||||
|
const count = 36;
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const x = (w * i) / (count - 1);
|
||||||
|
const raw = (this.analyserData[i] || 0) / 255;
|
||||||
|
const step =
|
||||||
|
Math.round((baseline - raw * (h * 0.48) + Math.sin(t * 1.6 + i * 0.35) * 3) / 2) * 2;
|
||||||
|
points.push({ x, y: step });
|
||||||
|
}
|
||||||
|
this.drawSmoothPath(ctx, points, p, 1.4, 0.2);
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = p;
|
||||||
|
ctx.globalAlpha = 0.88;
|
||||||
|
ctx.lineWidth = 2.1;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(points[0].x, points[0].y);
|
||||||
|
for (let i = 1; i < points.length; i++) {
|
||||||
|
ctx.lineTo(points[i].x, points[i - 1].y);
|
||||||
|
ctx.lineTo(points[i].x, points[i].y);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg.style === 'bioshipMembrane') {
|
||||||
|
const points = [];
|
||||||
|
const count = 28;
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const x = (w * i) / (count - 1);
|
||||||
|
const wave = Math.sin(t * 1.1 + i * 0.42) * (8 + e.bass * 18);
|
||||||
|
const flutter = Math.cos(t * 2.2 + i * 0.8) * (3 + e.treble * 6);
|
||||||
|
points.push({ x, y: baseline + wave + flutter });
|
||||||
|
}
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = 'rgba(16,185,129,0.16)';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, h);
|
||||||
|
ctx.lineTo(points[0].x, points[0].y);
|
||||||
|
for (let i = 1; i < points.length - 1; i++) {
|
||||||
|
const xc = (points[i].x + points[i + 1].x) / 2;
|
||||||
|
const yc = (points[i].y + points[i + 1].y) / 2;
|
||||||
|
ctx.quadraticCurveTo(points[i].x, points[i].y, xc, yc);
|
||||||
|
}
|
||||||
|
ctx.lineTo(points[points.length - 1].x, points[points.length - 1].y);
|
||||||
|
ctx.lineTo(w, h);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
ctx.restore();
|
||||||
|
this.drawSmoothPath(ctx, points, p, 2.6, 0.95);
|
||||||
|
const secondary = points.map((pt, i) => ({
|
||||||
|
x: pt.x,
|
||||||
|
y: pt.y + 10 + Math.sin(t * 1.7 + i * 0.5) * 6
|
||||||
|
}));
|
||||||
|
this.drawSmoothPath(ctx, secondary, a, 1.4, 0.45);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg.style === 'retroCrt') {
|
||||||
|
ctx.fillStyle = 'rgba(0,18,4,0.22)';
|
||||||
|
ctx.fillRect(0, 0, w, h);
|
||||||
|
this.drawWaveformGrid(ctx, w, h, p, 8, 5, 0.06);
|
||||||
|
for (let i = 0; i < h; i += 3) {
|
||||||
|
ctx.fillStyle = 'rgba(0,0,0,0.08)';
|
||||||
|
ctx.fillRect(0, i, w, 1);
|
||||||
|
}
|
||||||
|
const points = [];
|
||||||
|
const count = 34;
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const x = (w * i) / (count - 1);
|
||||||
|
const wave = Math.sin(t * 1.4 + i * 0.5) * (6 + e.mid * 10);
|
||||||
|
const drift = Math.sin(t * 0.25) * 6;
|
||||||
|
points.push({ x, y: baseline + drift + wave });
|
||||||
|
}
|
||||||
|
this.drawSmoothPath(ctx, points, p, 6, 0.12);
|
||||||
|
this.drawSmoothPath(ctx, points, p, 3.2, 0.24);
|
||||||
|
this.drawSmoothPath(ctx, points, p, 1.6, 0.96);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg.style === 'deepSpaceField') {
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = p;
|
||||||
|
ctx.globalAlpha = 0.15;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, baseline);
|
||||||
|
ctx.lineTo(w, baseline);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
const points = [];
|
||||||
|
const count = 30;
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const x = (w * i) / (count - 1);
|
||||||
|
const longWave = Math.sin(t * 0.35 + i * 0.2) * (e.bass * 24 + 8);
|
||||||
|
const interference = Math.cos(t * 1.1 + i * 1.4) * (2 + e.treble * 4);
|
||||||
|
points.push({ x, y: baseline + longWave + interference });
|
||||||
|
}
|
||||||
|
this.drawSmoothPath(ctx, points, p, 1.9, 0.8);
|
||||||
|
const ghost = points.map((pt, i) => ({
|
||||||
|
x: pt.x,
|
||||||
|
y: pt.y + 10 + Math.sin(t * 0.6 + i * 0.4) * 5
|
||||||
|
}));
|
||||||
|
this.drawSmoothPath(ctx, ghost, s, 1.1, 0.35);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg.style === 'comedyAdventure') {
|
||||||
|
const points = [];
|
||||||
|
const count = 28;
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const x = (w * i) / (count - 1);
|
||||||
|
const wave = Math.sin(t * 2.1 + i * 0.65) * (8 + e.mid * 16);
|
||||||
|
const bounce = Math.abs(Math.sin(t * 1.3 + i * 0.35)) * (2 + e.treble * 10);
|
||||||
|
points.push({ x, y: baseline + wave - bounce });
|
||||||
|
}
|
||||||
|
this.drawSmoothPath(ctx, points, p, 2.6, 0.9);
|
||||||
|
const secondary = points.map((pt, i) => ({
|
||||||
|
x: pt.x,
|
||||||
|
y: pt.y + 10 + Math.cos(t * 1.4 + i * 0.6) * 5
|
||||||
|
}));
|
||||||
|
this.drawSmoothPath(ctx, secondary, s, 1.5, 0.55);
|
||||||
|
ctx.fillStyle = a;
|
||||||
|
ctx.globalAlpha = 0.8;
|
||||||
|
for (let i = 3; i < points.length; i += 6) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(points[i].x, points[i].y - 8, 2.5 + e.treble * 2, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg.style === 'starflight') {
|
||||||
|
const traces = [
|
||||||
|
{ off: -8, amp: 8 + e.treble * 8, color: p, speed: 1.5 },
|
||||||
|
{ off: 0, amp: 6 + e.mid * 10, color: s, speed: 1.15 },
|
||||||
|
{ off: 8, amp: 4 + e.lowMid * 8, color: a, speed: 0.9 }
|
||||||
|
];
|
||||||
|
traces.forEach((trace, idx) => {
|
||||||
|
const points = [];
|
||||||
|
for (let i = 0; i < 30; i++) {
|
||||||
|
const x = (w * i) / 29;
|
||||||
|
const y = baseline + trace.off + Math.sin(t * trace.speed + i * 0.42 + idx) * trace.amp;
|
||||||
|
points.push({ x, y });
|
||||||
|
}
|
||||||
|
this.drawSmoothPath(
|
||||||
|
ctx,
|
||||||
|
points,
|
||||||
|
trace.color,
|
||||||
|
idx === 0 ? 2.2 : 1.3,
|
||||||
|
idx === 0 ? 0.92 : 0.58
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback should never be hit, but if it does keep a lively generic analyzer.
|
||||||
|
const bars = 28;
|
||||||
|
const barW = Math.max(3, w / bars - 2);
|
||||||
|
ctx.fillStyle = p;
|
||||||
for (let i = 0; i < bars; i++) {
|
for (let i = 0; i < bars; i++) {
|
||||||
const val = (this.analyserData[i * 2] || 0) / 255;
|
const val = (this.analyserData[i * 2] || 0) / 255;
|
||||||
const barH = Math.max(2, val * (h - 4));
|
const barH = Math.max(2, val * (h - 4));
|
||||||
@@ -2602,7 +3409,7 @@ class ObservationEngine {
|
|||||||
if (universeId === 'military' || universeId === 'outlaw' || universeId === 'industrial') {
|
if (universeId === 'military' || universeId === 'outlaw' || universeId === 'industrial') {
|
||||||
sequences.push(this.buildHullBreachSequence());
|
sequences.push(this.buildHullBreachSequence());
|
||||||
}
|
}
|
||||||
if (universeId === 'whoniverse') {
|
if (universeId === 'whataverse') {
|
||||||
sequences.push(this.buildTemporalAnomalySequence());
|
sequences.push(this.buildTemporalAnomalySequence());
|
||||||
}
|
}
|
||||||
if (universeId === 'bioships') {
|
if (universeId === 'bioships') {
|
||||||
@@ -2663,7 +3470,7 @@ class ObservationEngine {
|
|||||||
'FABRICATOR SYSTEMS ON STANDBY',
|
'FABRICATOR SYSTEMS ON STANDBY',
|
||||||
'SENSOR SWEEP: NO ANOMALIES DETECTED'
|
'SENSOR SWEEP: NO ANOMALIES DETECTED'
|
||||||
],
|
],
|
||||||
whoniverse: [
|
whataverse: [
|
||||||
'TEMPORAL GRACE PERIOD: ACTIVE',
|
'TEMPORAL GRACE PERIOD: ACTIVE',
|
||||||
'CLOISTER CHIME: SILENT',
|
'CLOISTER CHIME: SILENT',
|
||||||
'CHAMELEON CIRCUIT: STUCK (AS USUAL)',
|
'CHAMELEON CIRCUIT: STUCK (AS USUAL)',
|
||||||
@@ -2780,4 +3587,3 @@ class ObservationEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.ObservationEngine = ObservationEngine;
|
window.ObservationEngine = ObservationEngine;
|
||||||
|
|
||||||
|
|||||||
+41
-5
@@ -3,25 +3,61 @@
|
|||||||
Packages the modular SciFi-XZBT project into a single, standalone, offline HTML file.
|
Packages the modular SciFi-XZBT project into a single, standalone, offline HTML file.
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\package.ps1
|
.\package.ps1
|
||||||
|
.EXAMPLE
|
||||||
|
.\package.ps1 -Force
|
||||||
#>
|
#>
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[string]$OutputFile = ""
|
[string]$OutputFile = "",
|
||||||
|
[switch]$Force
|
||||||
)
|
)
|
||||||
|
|
||||||
$rootDir = (Resolve-Path "$PSScriptRoot\..").Path
|
$rootDir = (Resolve-Path "$PSScriptRoot\..").Path
|
||||||
$indexFile = Join-Path $rootDir "index.html"
|
$indexFile = Join-Path $rootDir "index.html"
|
||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace($OutputFile)) {
|
|
||||||
$OutputFile = Join-Path $rootDir "dist\SciFiAmbientDisplay_v4.html"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $indexFile)) {
|
if (-not (Test-Path $indexFile)) {
|
||||||
Write-Error "index.html not found in $rootDir"
|
Write-Error "index.html not found in $rootDir"
|
||||||
exit 1
|
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 "Packaging SciFi Ambient Display from $rootDir..." -ForegroundColor Cyan
|
||||||
|
Write-Host " -> Commit: $commitHash" -ForegroundColor Cyan
|
||||||
|
|
||||||
$indexLines = [System.IO.File]::ReadAllLines($indexFile)
|
$indexLines = [System.IO.File]::ReadAllLines($indexFile)
|
||||||
$output = [System.Collections.Generic.List[string]]::new()
|
$output = [System.Collections.Generic.List[string]]::new()
|
||||||
|
|||||||
Reference in New Issue
Block a user