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

19 KiB
Raw Permalink Blame History

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.