Files
SciFi-XZBT/visual_elements.md
ClaudeandClaude Sonnet 5 977d348c4f Rename in-app IP references and rewrite README for end users
Apply the ship/vessel and terminology rename mappings throughout the
live app (index.html, css/style.css, js/*.js) and reference docs, per
Ship_IP_Changes.md and Theme_Terminology_IP_Changes.md, including:
- Ship/theme names and generic terminology (Starfleet, LCARS, Warp
  Core, TARDIS, etc.) in both code strings and visible UI text, while
  leaving internal code identifiers (theme keys, CSS classes, preset
  IDs) untouched.
- Made "Species 8675309" canonical and fixed the dotted T.A.R.D.I.X.
  acronym on the Whataverse theme.
- Replaced the per-preset era/pulseShape franchise tag in the preset
  list with the existing safe universe category name, since those
  codes are also used functionally by the audio and observation
  engines and weren't covered by either mapping document.

Also rewrite README.md to lead with end-user ambience usage (how to
run it, universe/vessel overview, mixer channels, sleep timer,
observation mode, hotkeys) with the developer/build notes moved into
a collapsed section.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Jt872nc9Fi2WMaJhAKGyDq
2026-09-04 21:16:11 +00:00

323 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 | starflight | whataverse | 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. | `whataverse` |
| **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. | `starflight` |
| **Orbiting Moon** | `renderCelestialObjects()` | Small grey disc on a squashed ellipse (×1.7 / ×0.6) around the Class-M planet, with an offset shadow disc. | `starflight` |
| **Deep-Space Planet** | `regenerateDeepSpaceField()` / `renderDeepSpaceField()` | 24 per session from 6 distinct world palettes (gas giant, ice world, ion-storm giant, seismic rocky, biosignature rocky, frozen dormant). 50% chance of rings, 02 moons each, independent slow drift. | `deepspace` |
| **Nebula Cloud** | `renderDeepSpaceField()` | 35 soft morphing blobs, radius 220620 px, alpha 0.0350.09, each with its own drift, pulse and morph seed. | `deepspace` |
| **Dust Lane** | `renderDeepSpaceField()` | 47 thin angled streaks, 260620 px long, for texture distinct from the soft blobs. | `deepspace` |
| **Derelict Silhouette** | `renderDeepSpaceField()` | 70% chance of 12 segmented megastructure hulks with a blinking beacon and a `DERELICT // NO SIGNAL` callout. | `deepspace` |
| **Anomaly** | `renderDeepSpaceField()` | 12 per session, kind `lensing` or `radiation`, each gated behind its own random `minActivity` threshold of 0.150.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 whataverse, 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** | 35 patches, own palette, slow pulse. |
| **Panorama Planet** | 12 bodies, banded, 45% rings, 02 moons, own light angle. |
| **Distant Sun** | One, small radius, warm or cool tint. |
| **Sister Station** | 12 far-off structures with optional ring and blinking beacons. |
| **Asteroid Cluster** | 918 procedurally faceted rocks (58 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). Starflight gets a bespoke calm 150-star cool profile; everything else falls back to `DEFAULT_STARFIELD_PROFILE` (340 stars, 10 colors). | `starflight`, `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 1226 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()` | LCARD 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 / starflight |
| `cruiser` | `USS GIBRALTAR // NCC-#####` | starflight, military |
| `runabout` | `RUNABOUT YANGTZE // NCC-72452` | starflight |
| `tardix` | `TYPE 4D TIME CAPSULE // DRIFT` | whataverse |
| `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 |
| --- | --- |
| `starflight` | 2 flights (scout arrival, shuttle cruise), 2 orbits (sensor blip, diamond marker) |
| `whataverse` | 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 |
| --- | --- |
| `starflight` | contact, vector, data, ring, diagnostic, streak |
| `whataverse` | 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 2575% jitter.
Above activity 0.55 (telemetry) and 0.82 (any source) a second and third
concurrent effect can spawn, staggered 120380 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 starflight — by the active preset's *era*, so TOS gets a hexagonal amber bridge bezel and TNG/Wayfarer 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 4575 s
initially, then 90180 s after each one completes.
| Sequence | Steps | Universes |
| --- | --- | --- |
| **First Contact** | comet → unknown vessel + emphasis flyby → hailing frequencies → warp flash | starflight, 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 | whataverse |
| **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` (01 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`.