Files
Epic-The-Musical-Timeline/docs/ARCHITECTURE.md
T
Lucas WintherandClaude Opus 5.5 0456683d1b fix(app): controller cancels instead of queueing; Go ashore; focus, narrow and lyrics fixes
From the final review: a request cancelled mid-drop no longer sails under
the wrong frieze; new requests cancel running animations; Play voyage
no longer sticks during the first scene and stops on any other
navigation; the landing intro is revealed by the lift; Go ashore enters
a landing through the frieze; Back to sea keeps keyboard focus; the
timeline is compact below 900px; lyrics re-scroll after a tab switch;
the shared toon gradient is no longer disposed with a landing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 15:25:45 +01:00

7.3 KiB

Architecture

How epic-the-musical-timeline is built today. Update in the same commit as any structural change.

Overview

A Bun server (Bun.serve) with HTML-import routes serves the voyage app at /, the mockup gallery at /mockups/ and the design-round mockups at /mockups/NN-slug/, plus /content/* from an optional mounted content folder. Bun's bundler turns each HTML page and its imported TS/CSS into hashed assets. bun run build bundles the server and every page into dist/; the Docker image runs that bundle. The app reads the album data (src/data/album/); the mockups read the frozen sample dataset. Every page mounts one shared credits snippet.

Map

Path Responsibility
src/server.ts startServer(port, { contentDir? }): routes /, /mockups/NN-slug/ (+ 302 from the no-slash form), /content/* via src/content/routes.ts, 404 otherwise. Runs when import.meta.main; PORT env, default 3000.
src/mockups/index.html, index.ts, index.css Mockup gallery at /mockups/: static cards linking each mockup, then mountCredits().
src/mockups/registry.ts mockups: MockupInfo[] — slug, number, name, pitch, what to try.
src/mockups/NN-slug/ One design direction: index.html (links ../../shared/credits.css, has <footer data-fan-credits>), main.ts, style.css, plus optional helper modules (e.g. scene.ts, layout.ts).
src/data/types.ts Saga, Song, Part, Motif, StoryTime, OdysseyRef, Video.
src/data/sample.ts sagas, motifs, songs. Placeholder lyrics; unverified fields // TODO(verify).
src/data/query.ts songsWithMotif, motifsInSong, gapYears, sagaOf.
src/shared/credits.ts FAN_NOTICE, mountCredits(target?).
src/shared/audio-slot.ts createAudioSlot(song): <audio> for song.audio, else an empty-state slot.
src/shared/video-cards.ts createVideoCards(song): YouTube link cards, no embeds.
src/shared/credits.css Styles for credits, audio slot, video cards.
src/content/routes.ts contentResponse(req, dir): /content/library.json, /content/files/<path> (traversal-guarded, HTTP ranges), /content/homer/<odyssey|iliad>/<book>.json. All 404 when no folder is mounted.
src/content/library.ts scanLibrary(dir): sagas (story order) and songs from Songs and Lyrics/EPIC - <Saga> (<year>)/; .lrc preferred over .txt.
src/content/homer.ts splitBooks(text): Gutenberg Butler text → books (heading, summary, paragraphs).
src/shared/lrc.ts parseLrc(text): timed lyric lines, sorted.
src/app/index.html, main.ts, style.css The app page: stage (canvas, frieze layer, caption, timeline) and panel. main.ts wires store → controller → world/curtain, panel, timeline, play/prev/next, keyboard (←/→, Esc), the no-WebGL frieze stage. style.css holds the design tokens (see DESIGN.md).
src/app/state.ts AppState (songId, tab, motif, timeline, ribbons, read), parseState/stateToSearch (URL), createStore (push on song change, replace otherwise, popstate restores).
src/app/voyage.ts transitionFor(from, to): same saga → sail; adjacent saga → that saga's curtain; further → fade; plus the target's landing.
src/app/controller.ts A new request starts at once (world and curtain cancel their running animation; older steps stop at their next await). Tracks what is on screen (at, cover) so a step never sails under the wrong saga's frieze. Under a curtain: cover → instant sail → start the landing → horizon on the frieze ground line → lift. Play voyage (3.4s per song, silent; any other navigation stops it), Go ashore, Back to sea.
src/app/world/ createWorld(canvas) (null without WebGL 2): route.ts (9 places, 40 stops, pure), stylised islands, ship, ribbons ("threads on the sea"), landings/ cave, arena, hall.
src/app/frieze/ friezeSvg(sagaId) for the 9 sagas from a figure kit; createCurtain(layer): drop, lift (part along the ground line, or zoom through a doorway), fade, groundY().
src/app/panel/ mountPanel: About (facts, Odyssey reading view), Listen & lyrics (real player, line-synced lyrics, Watch), Paths (strip when the timeline is hidden, motif rows, nine-place chart), Fan works.
src/app/timeline/ layoutTimeline (pure: current saga expanded, others compact) and mountTimeline: stops, you-are-here, threads, hide/show, "Threads on the sea".
src/app/content.ts Client for /content/*: library match by normalised title, lyrics (.lrc timed / .txt), Homer books, passage start by keyword.
src/data/album/ sagas, songs (40, album order, landing), motifs (character themes derived from singers, except Odysseus), fanWorks (empty in v1), query helpers.
tests/ data.test.ts (dataset invariants), shared.test.ts (DOM modules under happy-dom), server.test.ts (every route 200s, index links every registry slug, each mockup has the credits hook), content.test.ts (LRC, library scan, Homer split, /content/* routes on a temp fixture), album.test.ts, state.test.ts, voyage.test.ts (transitions, controller with fakes), route.test.ts, frieze.test.ts, timeline.test.ts, panel.test.ts, app-content.test.ts.
Dockerfile Two-stage oven/bun:1.3: install + build, then copy dist/ and run from WORKDIR /app/dist.

Data flow

  1. Request hits Bun.serve; an HTML-import route returns the bundled page (dev: bundled on demand with HMR via bun --hot; production: prebuilt assets from dist/).
  2. The page's main.ts imports src/data/* and src/shared/* (bundled in) and calls mountCredits().
  3. App: the URL becomes AppState; every change goes through the store. The controller moves the stage (curtain + world), and the panel and timeline re-render from the store. The panel fetches /content/library.json, the song's audio and lyrics, and Homer books on demand; all of it is optional.

External dependencies

Optional at runtime: the content folder named by CONTENT_DIR (real audio, lyrics, cover art, the Odyssey and Iliad: Butcher & Lang / Lang, Leaf & Myers preferred, Butler as fallback). It is mounted read-only, never committed or baked into the image (spec 003). Without it the site works and /content/* answers 404. Otherwise none. Fonts (@fontsource/*) and three.js are bundled from npm. Outbound links only: YouTube (video cards) and Project Gutenberg (Odyssey references) — the site works without them.

Invariants

  • Every page renders FAN_NOTICE via mountCredits().
  • src/server.ts routes, src/mockups/registry.ts and the cards in src/mockups/index.html list the same slugs (tests/server.test.ts).
  • Every motif id used by a part exists; every motif appears in at least one song (tests/data.test.ts).
  • No two mockups ship byte-identical style.css (the build fails otherwise).
  • The album has 40 songs whose titles and order match the content folder (tests/album.test.ts).
  • No lyric text in the repo; lyrics are fetched from /content/ at runtime only.

Known rough edges

  • The production bundle only runs with dist/ as the cwd (bun run start, Docker WORKDIR /app/dist); bun dist/server.js from the repo root crashes. See docs/TODO.md.
  • Routes, registry and index cards are three hand-maintained lists. See docs/TODO.md.