- Add sound_reference.md (937 lines): Real-world production reference covering Star Trek (TOS–ENT), Doctor Who/Whoniverse, Bioships (six ships across five franchises), and Space Stations (six stations across five sources). - Add agents.md: Project architecture guide (file ownership, load order, conventions). - Add telemetry_elements.md: Catalog of 10 telemetry elements with per-era selection weights and preset-by-preset routing. - Add visual_elements.md: Canvas/SVG split architecture, layer declarations, per-theme casts. - Fix duplicate soundboard mapping: Separate 'AIR HANDLER THUD' from 'DOCKING CLAMP LATCH'. Create ExpandedSciFiAudioSynth.synthesizeAirHandlerThud() (dull triangle-wave thump + sub-octave + slow airflow whoosh) distinct from synthesizeDockingClamp() (bright square-wave impact + pneumatic hiss). Update js/app.js to wire btn-air-handler to the new method. Key findings: * All 70 presets across 10 universes use Star Trek telemetry eras only — cross-universe borrowing is structural, not accidental. * Doctor Who TARDIS demat correctly implements Brian Hodgson's 1963 technique (piano strings + tape feedback). * Sevastopol Station's production sound design (Jeff van Dyck, Pinewood foley) is the best-documented non-Trek entry. * The Expanse's "jury-rigged" Belter signature (Nelson Ferreira) is the single most actionable production detail found. Co-Authored-By: Claude Haiku 4.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NbMozG2xjcgLBia8vrzTrr
323 lines
19 KiB
Markdown
323 lines
19 KiB
Markdown
# Observation Visual Elements
|
||
|
||
Catalog of the animated visual elements Observation Mode produces. Everything
|
||
here is drawn procedurally at runtime — canvas 2D in `js/observation-engine.js`,
|
||
inline SVG generated in `js/app.js` and `js/observation-bezels.js`, and CSS
|
||
keyframes in `css/style.css`. There are no image assets in this project.
|
||
|
||
---
|
||
|
||
## The two rendering paths
|
||
|
||
Observation Mode composites **two independent stacks**, and which one a universe
|
||
uses is the single most important fact when deciding where a new element goes.
|
||
|
||
| | Canvas path | SVG path |
|
||
| --- | --- | --- |
|
||
| Owner | `ObservationEngine.render()` | `getObservationSvg()` / `buildObservationSceneMarkup()` in `js/app.js` |
|
||
| DOM node | `#observation-canvas` | `#observation-stage` (art) + `#observation-sim-layer` (motion), z-index 1 and 2 |
|
||
| Motion | per-frame in a `requestAnimationFrame` loop | CSS animations + JS spline morphing |
|
||
| Used by | universes with `canvas: true` | every universe, but it is the *only* path for `canvas: false` ones |
|
||
|
||
`OBSERVATION_CANVAS` (top of `js/observation-engine.js`) is the manifest. A
|
||
universe not declared `canvas: true` draws nothing on the canvas at all — three
|
||
separate barriers enforce this (`canvasEnabled()` in `render()`, canvas
|
||
`visibility: hidden` in `start()`, and the stage SVG's own opaque background
|
||
rect). Its observation display is entirely bespoke SVG art.
|
||
|
||
### Layer declarations
|
||
|
||
| Layer | starfleet | whoniverse | deepspace | spacestations | others |
|
||
| --- | :-: | :-: | :-: | :-: | :-: |
|
||
| `celestial` | ● | ● | | | |
|
||
| `starfield` | ● | | ● | | |
|
||
| `deepSpaceField` | | | ● | | |
|
||
| `constellations` | | | ● | | |
|
||
| `shootingStars` | | | ● | ● | |
|
||
| `stationPanorama` | | | | ● | |
|
||
| `traffic` | | | | ● | |
|
||
| `events` | | | ● | ● | |
|
||
| `reticles` | | | | ● | |
|
||
| `cinematics` | | | | ● | |
|
||
| canvas enabled | ● | ● | ● | ● | — |
|
||
|
||
`industrial`, `bioships`, `retrofuture`, `military`, `outlaw` and `comedy` are
|
||
all `canvas: false` with no layers.
|
||
|
||
**Dormant layer:** `renderNebulae()` exists and is fully implemented with a
|
||
per-universe palette, but no universe currently declares `nebulae`. It is
|
||
available for reuse without new code.
|
||
|
||
Draw order in `render()` is fixed and should not be reshuffled:
|
||
stationPanorama → nebulae → celestial → deepSpaceField → starfield →
|
||
constellations → shootingStars → traffic/events → reticles.
|
||
|
||
---
|
||
|
||
## Canvas celestial elements
|
||
|
||
| Element | Where drawn | Description | Where used |
|
||
| --- | --- | --- | --- |
|
||
| **Time Vortex** | `renderCelestialObjects()` | 14 nested ellipses receding on a z-cycle, alternating cyan and gold strokes, counter-rotating by ring parity. Deliberately drawn on pure black with no starfield. | `whoniverse` |
|
||
| **Class-M Planet** | `renderCelestialObjects()` | Large body with a Rayleigh-scattering atmosphere halo, a radial terminator gradient, and a bright rim arc on the lit limb. | `starfleet` |
|
||
| **Orbiting Moon** | `renderCelestialObjects()` | Small grey disc on a squashed ellipse (×1.7 / ×0.6) around the Class-M planet, with an offset shadow disc. | `starfleet` |
|
||
| **Deep-Space Planet** | `regenerateDeepSpaceField()` / `renderDeepSpaceField()` | 2–4 per session from 6 distinct world palettes (gas giant, ice world, ion-storm giant, seismic rocky, biosignature rocky, frozen dormant). 50% chance of rings, 0–2 moons each, independent slow drift. | `deepspace` |
|
||
| **Nebula Cloud** | `renderDeepSpaceField()` | 3–5 soft morphing blobs, radius 220–620 px, alpha 0.035–0.09, each with its own drift, pulse and morph seed. | `deepspace` |
|
||
| **Dust Lane** | `renderDeepSpaceField()` | 4–7 thin angled streaks, 260–620 px long, for texture distinct from the soft blobs. | `deepspace` |
|
||
| **Derelict Silhouette** | `renderDeepSpaceField()` | 70% chance of 1–2 segmented megastructure hulks with a blinking beacon and a `DERELICT // NO SIGNAL` callout. | `deepspace` |
|
||
| **Anomaly** | `renderDeepSpaceField()` | 1–2 per session, kind `lensing` or `radiation`, each gated behind its own random `minActivity` threshold of 0.15–0.55. | `deepspace` |
|
||
| **Computer Callout** | `drawComputerCallout()` | Shared annotation primitive: scanning bracket, periodic ping ring, leader line and label. Attached to every planet, derelict and anomaly. | `deepspace` |
|
||
| **Generic Nebulae** | `renderNebulae()` | 4 ambient morphing blobs with a per-universe palette (`getNebulaPalette()` covers whoniverse, industrial, bioships and a default). | *dormant — no universe declares it* |
|
||
|
||
### Station panorama elements
|
||
|
||
The `spacestations` sky is a **360° panorama in azimuth/elevation space**, not a
|
||
forward-motion starfield: the station rotates in place, one full revolution every
|
||
240 s, with a ~22% field of view. Generated once per page load.
|
||
|
||
| Element | Description |
|
||
| --- | --- |
|
||
| **Panorama Starfield** | 900 stars around the full circle, power-curve size distribution (mostly pinpricks), per-star twinkle speed and phase. |
|
||
| **Panorama Nebulae** | 3–5 patches, own palette, slow pulse. |
|
||
| **Panorama Planet** | 1–2 bodies, banded, 45% rings, 0–2 moons, own light angle. |
|
||
| **Distant Sun** | One, small radius, warm or cool tint. |
|
||
| **Sister Station** | 1–2 far-off structures with optional ring and blinking beacons. |
|
||
| **Asteroid Cluster** | 9–18 procedurally faceted rocks (5–8 vertices each) with individual rotation. |
|
||
|
||
Major features are **slot-spread** around the circle rather than randomly placed,
|
||
so one notable object drifts through the window every so often instead of
|
||
everything clumping into one bearing.
|
||
|
||
---
|
||
|
||
## Canvas motion elements
|
||
|
||
| Element | Method | Description | Where used |
|
||
| --- | --- | --- | --- |
|
||
| **Parallax Starfield** | `renderStarfield()` | 3D projected star volume with per-universe profile (count, size range, color set, warp-streak flag). Starfleet gets a bespoke calm 150-star cool profile; everything else falls back to `DEFAULT_STARFIELD_PROFILE` (340 stars, 10 colors). | `starfleet`, `deepspace` |
|
||
| **Warp Streaks** | `renderStarfield()` + `render()` | In `warp` flight mode stars elongate and the frame clears to a translucent fill instead of solid, producing a motion-blur trail. Station view never smears. | starfield universes |
|
||
| **Shooting Star** | `spawnShootingStar()` / `updateShootingStars()` / `renderShootingStars()` | Occasional fast streak with a fading tail. | `deepspace`, `spacestations` |
|
||
| **Constellation Lines** | `regenerateConstellation()` / `renderConstellations()` | Faint synthetic point set with connecting lines that fade in, hold, and fade out. Independent of the main starfield. | `deepspace` |
|
||
| **Traffic Vessel** | `spawnTraffic()` / `drawShipVessel()` | A ship crossing the frame on a linear path over 12–26 s, with an engine particle trail. 12% chance each of a warp-flash entry and/or warp-jump exit. | `spacestations` (and any universe declaring `traffic`) |
|
||
| **Comet** | `spawnEvent('comet')` | 5 s drifting event with lateral velocity. | `deepspace`, `spacestations`, cinematics |
|
||
| **Warp Flash** | `spawnEvent('warp-flash')` | 1.4 s stationary flash, also used as the arrival marker for warp-entry traffic. | `deepspace`, `spacestations`, cinematics |
|
||
| **Target Reticle** | `renderTargetReticles()` | LCARS corner brackets around each traffic vessel with a leader line and text tag. | `spacestations` |
|
||
|
||
### Traffic vessel types
|
||
|
||
`drawShipVessel()` draws each by type. `spawnTraffic()` picks the type and label
|
||
from the active universe:
|
||
|
||
| Type | Label pattern | Universe |
|
||
| --- | --- | --- |
|
||
| `shuttle` | `SHUTTLE // TYPE-9` | default / starfleet |
|
||
| `cruiser` | `USS GIBRALTAR // NCC-#####` | starfleet, military |
|
||
| `runabout` | `RUNABOUT YANGTZE // NCC-72452` | starfleet |
|
||
| `tardis` | `TYPE 40 TIME CAPSULE // DRIFT` | whoniverse |
|
||
| `freighter` | `HEAVY HAULER // CLASS IV` | industrial, outlaw |
|
||
| `fighterwing` | `VIPER WING // FLIGHT n` | military |
|
||
| `bioshippod` | `SPAWN POD // DRIFTING` | bioships |
|
||
| `retrosaucer` | `ATOMIC CRUISER // SAUCER CLASS` | retrofuture |
|
||
|
||
Note that most of these universes are `canvas: false`, so their vessel type is
|
||
defined but only reachable if `traffic` is later declared for them. The drawing
|
||
code already exists.
|
||
|
||
---
|
||
|
||
## SVG scene elements
|
||
|
||
`buildObservationSceneMarkup(theme)` populates `#observation-sim-layer` with
|
||
motion entities over the bespoke stage art. Three builders cover every case:
|
||
|
||
| Builder | Motion | Parameters |
|
||
| --- | --- | --- |
|
||
| `obsFlightMarkup` | Travel along a cubic Bézier | `x0..x3, y0..y3, duration, delay, mode` (`arrival` / `cruise` / `depart`), `scale0→scale1`, `minActivity`, `opacity` |
|
||
| `obsOrbitMarkup` | Elliptical orbit | `cx, cy, rx, ry, duration, phase, scale0→scale1`, `minActivity`, `opacity` |
|
||
| `obsFloatMarkup` | Bounded drift around a point | `cx, cy, ampX, ampY, duration, phase`, `minActivity`, `opacity` |
|
||
|
||
Per-theme casts:
|
||
|
||
| Theme | Entities |
|
||
| --- | --- |
|
||
| `starfleet` | 2 flights (scout arrival, shuttle cruise), 2 orbits (sensor blip, diamond marker) |
|
||
| `whoniverse` | 3 orbiting glyphs (◎ ∆ ∞), 2 floating vortex fragments |
|
||
| `industrial` | 4 camera-feed-clipped groups: bay drone, 3 embers, airlock tell-tale, corridor lamp — each parented to a clip path matching one feed's picture area |
|
||
| `bioships` | 3 flowing particles, 1 orbiting node, 1 breathing membrane |
|
||
| `retrofuture` | 2 vector-outline ships, 1 orbiting scope blip, 1 Lissajous figure |
|
||
| `military` | 1 four-ship formation, 1 hostile contact, 1 orbiting CAP marker |
|
||
| `deepspace` | 1 very slow distant ship, 1 comet, 1 large drifting ringed planet |
|
||
| `outlaw` | 1 runner, 1 pursuer, 1 floating needle gauge |
|
||
| `spacestations` | 5 clipped entities: arrival, departure, freighter, holding pattern, near pass |
|
||
| `comedy` | 1 tour ship, 1 orbiting `?`, 1 tumbling cube |
|
||
|
||
**Clipping is a rule, not a detail.** The sim layer sits *above* the stage art
|
||
(z-index 2 vs 1), so any entity that should appear inside a viewport, camera feed
|
||
or window must be wrapped in a matching `clipPath`. The industrial theme's
|
||
comment records exactly what went wrong when entities floated free in screen
|
||
space.
|
||
|
||
### Spline morphing
|
||
|
||
`initializeObservationSplineMorphs()` and `updateObservationSplineMorphs()`
|
||
parse the stage SVG's path data and continuously re-target control points, so
|
||
static-looking bespoke art breathes. `obsThemeSplineScale(theme)` sets the
|
||
per-theme amplitude; `pulseObservationSplineMorphs()` kicks it on activity.
|
||
|
||
---
|
||
|
||
## Transient activity effects
|
||
|
||
`triggerObservationActivity(source)` spawns short-lived generative overlays.
|
||
Each universe has its own six-effect vocabulary:
|
||
|
||
| Universe | Effect vocabulary |
|
||
| --- | --- |
|
||
| `starfleet` | contact, vector, data, ring, diagnostic, streak |
|
||
| `whoniverse` | echo, glyphs, coordinate, warp, rings |
|
||
| `industrial` | signal, dropout, vapour, motion, gain, warning |
|
||
| `bioships` | neural, spores, ripple, tendril, organ, metric |
|
||
| `retrofuture` | blip, scope, counter, vector, bloom, reel |
|
||
| `military` | contact, intercept, formation, sector, status, sweep |
|
||
| `deepspace` | anomaly, comet, lens, spectral, planet, signal |
|
||
| `outlaw` | glitch, route, contact, signal, gauge, rear |
|
||
| `spacestations` | dock, depart, guidance, traffic, beacon, queue |
|
||
| `comedy` | route, oddity, planet, status, contact, geometry |
|
||
|
||
Shared primitives every generator can reuse: `obsTextCard()`, `obsContact()`,
|
||
`obsExpandingRing()`, plus `obsRand` / `obsInt` / `obsPick` / `obsHex` and the
|
||
easing helpers.
|
||
|
||
**Two trigger sources:**
|
||
|
||
- `'telemetry'` — driven by the `scifi-telemetry-activity` audio event. Gated by
|
||
`responseChance = 0.18 + activity * 0.82`, so at low activity many audio
|
||
pulses pass silently and at maximum every pulse gets a visible response.
|
||
- `'ambient'` — a self-rescheduling timer whose interval curves from 12.5 s at
|
||
minimum activity down to 1.5 s at maximum, plus 25–75% jitter.
|
||
|
||
Above activity 0.55 (telemetry) and 0.82 (any source) a second and third
|
||
concurrent effect can spawn, staggered 120–380 ms apart.
|
||
|
||
---
|
||
|
||
## Frame, chrome and post effects
|
||
|
||
| Element | Where | Description | Where used |
|
||
| --- | --- | --- | --- |
|
||
| **Viewport Bezel** | `ObservationBezels.getViewportFrameSvg()` | Procedural SVG window frame, keyed by universe and — for starfleet — by the active preset's *era*, so TOS gets a hexagonal amber bridge bezel and TNG/Voyager get their own. | all except `spacestations`, which has its own native station window in the stage art |
|
||
| **Support Pillars** | inside each bezel | Optional vertical struts across the viewport. **Default off.** | bezel universes |
|
||
| **Glass Sheen** | `.observation-glass-sheen` | Static specular sheen over the viewport. | all |
|
||
| **Scanlines** | `.observation-scanlines` + `updateScanlineBreathing()` | CRT scanline overlay whose opacity breathes with audio energy (base 0.14). | all |
|
||
| **Vignette** | `.observation-vignette` | Edge darkening. | all |
|
||
| **Alert Wash** | `.observation-alert-wash.alert-red` / `.alert-yellow` | Full-screen color wash driven by `AlertSynth` state via `updateAlertState()`. | all |
|
||
| **Camera Wobble** | `cameraWobble` in `update()` | Slow sway of the projection center, so the canvas never feels locked to the frame. | canvas universes |
|
||
| **Viewport Vibration** | `updateViewportVibration()` | Short high-frequency shake driven by bass energy and warp pulses. | canvas universes |
|
||
| **Lighting Cycle** | `updateLightingCycle()` / `getLightingModifiers()` | A 25-minute ambient cycle that modulates overall brightness and tint — the slowest animation in the app. | canvas universes |
|
||
| **Waveform** | `renderWaveform()` | 30-bar HUD spectrum on its own small canvas, colored from the live `--primary-accent` CSS variable. | all |
|
||
| **Status Ticker** | `getTickerMessages()` / `updateStatusTicker()` | Scrolling HUD line, per-universe message sets. | all |
|
||
| **Cinematic Caption** | `queueCinematicAction('flash-status')` | Transient headline text driven by the cinematic director. | cinematic universes |
|
||
|
||
### Cinematic sequences
|
||
|
||
`updateCinematicDirector()` fires a timed multi-step sequence every 45–75 s
|
||
initially, then 90–180 s after each one completes.
|
||
|
||
| Sequence | Steps | Universes |
|
||
| --- | --- | --- |
|
||
| **First Contact** | comet → unknown vessel + emphasis flyby → hailing frequencies → warp flash | starfleet, deepspace, spacestations |
|
||
| **Hull Breach** | warp pulse + stress caption → red alert → damage control → stabilized + alert off | military, outlaw, industrial |
|
||
| **Temporal Anomaly** | flux detected → random warp flash → vortex stabilizing | whoniverse |
|
||
| **Bio Resonance** | resonant pulse → comet → dissipating | bioships |
|
||
| **Close Flyby** | one emphasized flyby (×1.6 scale, ×0.55 duration) | **every** universe |
|
||
|
||
Note that Hull Breach reaches into `AlertSynth` — a cinematic can change audio
|
||
state, and it restores it in its final step.
|
||
|
||
### Reusable CSS animation classes
|
||
|
||
Apply these to any SVG element instead of writing new keyframes:
|
||
|
||
| Class | Effect |
|
||
| --- | --- |
|
||
| `.obs-spin` | 18 s linear rotation |
|
||
| `.obs-spin-slow` | 42 s linear rotation |
|
||
| `.obs-spin-rev` | 28 s reverse rotation |
|
||
| `.obs-pulse` | 3.8 s ease-in-out scale pulse |
|
||
| `.obs-breathe` | 5.5 s ease-in-out soft swell |
|
||
| `.obs-flicker` | 8 s stepped flicker |
|
||
| `.obs-blink` | 3.5 s stepped blink |
|
||
| `.obs-dashflow` | flowing dash offset (20/12 dasharray, 7 s) |
|
||
|
||
All of them set `transform-box: fill-box` and a center origin where relevant.
|
||
A `prefers-reduced-motion` block collapses every animation inside
|
||
`.observation-overlay` to a single 1 ms iteration.
|
||
|
||
---
|
||
|
||
## Cross-cutting rules
|
||
|
||
**Coordinate systems — three of them, do not mix:**
|
||
|
||
1. **SVG stage space** — a fixed `viewBox="0 0 1600 900"` with
|
||
`preserveAspectRatio` slice behavior, so it lands like a CSS `cover`
|
||
background. All stage and sim-layer coordinates are in this space.
|
||
2. **Canvas normalized space** — deep-space objects store `nx` / `ny` as
|
||
screen fractions from the center and are projected each frame, so they
|
||
survive window resizes.
|
||
3. **Panorama azimuth/elevation** — station features store `a` (0–1 of a full
|
||
revolution) and `e` (elevation), projected through `stationProjectX/Y()`.
|
||
|
||
`getStationViewportRect()` reproduces the stage SVG's slice math so canvas
|
||
objects transit through the *actual* window opening rather than the full canvas.
|
||
|
||
**Session-seeded vs. per-frame.** The station panorama and the deep-space field
|
||
are generated **once per page load** in the constructor and retained for the
|
||
whole session — they survive entering and leaving Observation Mode, preset
|
||
switches and window resizes. Only a page reload rolls a new sky. Everything else
|
||
(traffic, events, shooting stars, constellations, transients) is spawned and
|
||
discarded continuously. Respect this contract: regenerating a seeded field
|
||
mid-session is a visible glitch, not a refresh.
|
||
|
||
**Placement clearance.** `placeAway()` in `regenerateDeepSpaceField()` biases new
|
||
objects away from already-placed ones, measuring in real pixels (converting
|
||
normalized offsets through canvas width/height, because a canvas is wider than
|
||
it is tall) and requiring clearance from **both** sides of a pair. The station
|
||
panorama does the equivalent with even bearing slots. Any new annotated object
|
||
must go through the same placement, or its callout will collide with another's.
|
||
|
||
**Activity gating.** Nearly every non-essential element carries a `minActivity`
|
||
threshold and is skipped when the observation activity slider sits below it.
|
||
This is how one scene serves both "calm ambient wallpaper" and "busy bridge".
|
||
Give every new element a threshold; 0.05 for things that should almost always be
|
||
present, 0.5+ for things that should feel like a rare event.
|
||
|
||
---
|
||
|
||
## Reuse or invent
|
||
|
||
**Reuse** when the need is structural rather than thematic. The three SVG motion
|
||
builders, `drawComputerCallout()`, `obsContact()` / `obsExpandingRing()` /
|
||
`obsTextCard()`, the eight CSS animation classes, the existing eight vessel
|
||
types, and the dormant `renderNebulae()` layer all cover a wide range of needs
|
||
with no new code. Declaring an existing layer for another universe in
|
||
`OBSERVATION_CANVAS` is the cheapest possible addition — the drawing code
|
||
already runs.
|
||
|
||
**Invent** when a universe needs identity no existing element carries. Then:
|
||
|
||
1. Decide the path first — canvas or SVG — from the universe's
|
||
`OBSERVATION_CANVAS` entry. Putting a canvas element in a `canvas: false`
|
||
universe produces nothing at all.
|
||
2. For a canvas layer: add it to the manifest, add a `hasLayer()` guard in
|
||
`render()` at the right point in the draw order, and never assume a layer is
|
||
universal.
|
||
3. For an SVG entity: use one of the three builders, give it a `minActivity`,
|
||
and clip it if it belongs inside a framed area.
|
||
4. Seed anything expensive once and store it on the engine, following the
|
||
panorama / deep-space contract.
|
||
5. Add a `minActivity` threshold and, for annotated objects, route placement
|
||
through the clearance helper.
|
||
6. Prefer the existing CSS animation classes over new keyframes, and confirm the
|
||
result still reads correctly under `prefers-reduced-motion`.
|