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
19 KiB
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() |
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 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 | 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). 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 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() |
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 thescifi-telemetry-activityaudio event. Gated byresponseChance = 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 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 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 | 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:
- SVG stage space — a fixed
viewBox="0 0 1600 900"withpreserveAspectRatioslice behavior, so it lands like a CSScoverbackground. All stage and sim-layer coordinates are in this space. - Canvas normalized space — deep-space objects store
nx/nyas screen fractions from the center and are projected each frame, so they survive window resizes. - Panorama azimuth/elevation — station features store
a(0–1 of a full revolution) ande(elevation), projected throughstationProjectX/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:
- Decide the path first — canvas or SVG — from the universe's
OBSERVATION_CANVASentry. Putting a canvas element in acanvas: falseuniverse produces nothing at all. - For a canvas layer: add it to the manifest, add a
hasLayer()guard inrender()at the right point in the draw order, and never assume a layer is universal. - For an SVG entity: use one of the three builders, give it a
minActivity, and clip it if it belongs inside a framed area. - Seed anything expensive once and store it on the engine, following the panorama / deep-space contract.
- Add a
minActivitythreshold and, for annotated objects, route placement through the clearance helper. - Prefer the existing CSS animation classes over new keyframes, and confirm the
result still reads correctly under
prefers-reduced-motion.