Storytelling

Astrarium’s storytelling stack lets the simulation be scripted as a sequence of axis tweens — voyages cinématiques (#83) that compose camera + scale + time + model + geo-anchor moves to tell a story. Each axis exposes the same imperative API:

setX(value, { duration, curve })  // tween, returns Promise
onXChange(cb)                     // subscription

This document records the cross-axis conventions — rules every axis honors. Per-axis details live in each axis’s source file and its WI’s design spec.

The five axes

# Axis Module Issue State
1 Camera src/camera/{brain,vcam,body,aim,storytelling,…}.js #80 / #96 shipped
2 Scale src/solar-system/state/scale.js #91 planned
3 Time src/solar-system/state/time.js #94 this WI
4 Model src/solar-system/state/model.js #93 shipped
5 Geo-anchor (TBD) #95 (depends on #92) planned

Cancellation policy

(Locked by the 2026-05-03 storytelling-v1 review.)

Cinema-protected mode (per-voyage, owned by #83): inputs are gated at the source during a voyage, so the cancellation policy never fires from user actions. The axis modules behave identically; #83’s protection layer just intercepts UI events before they reach the setter.

Date resolution

Voyages can reference dates as either an absolute Julian Date or a relative phenomenological token. The time module exposes:

resolveRelativeJD(token, currentJD) → number

Throws RangeError for unknown tokens or malformed planet ids.

Token allowlist v1

Category Tokens
Seasons nextWinterSolstice, nextSummerSolstice, nextSpringEquinox, nextAutumnEquinox
Eclipses nextSolarEclipse, nextLunarEclipse
Planet events nextOpposition:<planet>, nextInferiorConjunction:<planet>, nextSuperiorConjunction:<planet>, nextTransit:<planet>

<planet> is one of mercury, venus, mars, jupiter, saturn, uranus, neptune (Earth excluded — opposition is geocentric). Tokens dispatch to scanners in src/physics/events.js. Scanner-window failures (no event found within the default search window) also raise RangeError.

Season tokens are hemisphere-aware (Northern Hemisphere convention): nextWinterSolstice resolves to the December solstice, nextSummerSolstice to June, nextSpringEquinox to March, nextAutumnEquinox to September. The internal mapping to the hemisphere-neutral scanner kinds in events.js lives in solar-system-time.js.

Voyage authors compose:

await setTime(resolveRelativeJD('nextSolarEclipse', timeState.jd), {
  duration: 6000,
  curve: 'easeOutCubic',
});

Cross-axis interactions

TBD — fills in as #91 (scale), #93 (model), #95 (geo-anchor), #96 (camera-storytelling adapter) land. Initial topics:

dest-city:*:* vcam namespace (camera × geo-anchor)

The 11 curated cities × 11 modes (above / N / S / E / W / sunset / sunrise / moonrise / moonset / antisolar / panorama) are registered as dest-city:<city>:<mode> in src/views/solar-system.js. Voyage authors compose:

await flyToVcam(brain, 'dest-city:tokyo:sunrise', { duration: 6000 });

Modes sunrise/moonrise/moonset/antisolar/panorama have no UI button — they are voyage-only. The mode-then-city UI exposes above / N / S / E / W / sunset.

flyToVcam (in src/camera/storytelling.js) is the Promise-grammar adapter that voyages await — see docs/camera.md § Storytelling consumption.

Per-body overrides

(Locked by the 2026-05-04 #91 brainstorm.)

The scale axis introduces the canonical pattern for overriding a body-specific value on top of a global:

setBodyScale(id, { size?, distance?, exaggeration? } | null, { duration?, curve? }?)

Conventions every per-body override should honor:

The model axis (#93) is expected to mirror this with setBodyParam(id, …) — same partial-update, same null-to-clear, same per-key cancellation. Diverging would force voyage authors to learn two override grammars.

Roadmap

The Storytelling v1 milestone (#83) is delivered in four phases — see the milestone description on GitLab for current state. The time axis (#94, this WI) is phase 1 alongside the camera axis (#80, shipped) and the mini-PoC voyage in #83.

← Astrarium