docs: the voyage app in DESIGN, PRODUCT, AGENTS, ARCHITECTURE, DECISIONS, TODO

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas Winther
2026-10-30 15:12:53 +01:00
co-authored by Claude Opus 5.5
parent d355b3a41f
commit a196c09b82
6 changed files with 117 additions and 45 deletions
+10 -6
View File
@@ -1,7 +1,8 @@
# EPIC: The Musical Timeline
Unofficial fan site: an interactive timeline of *EPIC: The Musical* songs. Currently 7 design-direction
mockups. Bun 1.3.14 (server, bundler, test runner), TypeScript, three.js (mockups 06/07/13–17 only), Docker.
Unofficial fan site: an interactive timeline of *EPIC: The Musical* songs as a 3D sea voyage (the app at
`/`, `src/app/`), plus the design-round mockups at `/mockups/`. Bun 1.3.14 (server, bundler, test runner),
TypeScript, three.js (the app; mockups 06/07/13–17), Docker.
`AGENTS.md` is the single source of truth for agents here. `CLAUDE.md` and `GEMINI.md` only point here.
Deeper context on demand: `docs/ARCHITECTURE.md`, `docs/DECISIONS.md`, `docs/TODO.md`.
@@ -33,7 +34,7 @@ bun dist/server.js # wrong: crashes with 'Bundled file "./index-….js" no
"Multiple files share the same output path". Give each one its own styles from the start.
- Real audio, lyrics and cover art are Jorge Rivera-Herrans' copyrighted work: they live only in the
mounted content folder (`CONTENT_DIR`, served under `/content/`), never in git or the Docker image.
- Sample data (`src/data/sample.ts`): lyrics are placeholders, never real lyrics. Any fact, story time
- Album data (`src/data/album/`) and sample data (`src/data/sample.ts`): never real lyrics. Any fact, story time
or Odyssey reference you can't confirm gets `// TODO(verify)`.
- Assets are bundled, never loaded from a CDN: fonts from `@fontsource/*`, three.js from npm.
YouTube videos are link cards (`createVideoCards`), not embeds.
@@ -52,7 +53,7 @@ Tick plan checkboxes as you go so an interrupted session is resumable.
Trivial changes (typo, version bump, one-line config): say "trivial, skipping spec" and proceed.
Use impeccable for user-facing UI — `craft` for new, `audit`/`polish` on existing, `harden` before
shipping. Design context: `PRODUCT.md`, `DESIGN.md`. No visual direction is chosen yet (see `DESIGN.md`).
shipping. Design context: `PRODUCT.md`, `DESIGN.md` (the chosen direction: tokens, type, layout, 3D style).
Mockup rounds — any UI change beyond a trivial tweak, while brainstorming and before the spec:
- Round 1: about three genuinely different alternatives, each a clickable mockup with realistic
@@ -68,8 +69,11 @@ Mockup rounds — any UI change beyond a trivial tweak, while brainstorming and
## Architecture
`src/server.ts` -> `Bun.serve` with HTML-import routes: `/` and `/mockups/NN-slug/`
`src/data/` -> `types.ts` (shapes), `sample.ts` (shared sample data), `query.ts` (helpers)
`src/server.ts` -> `Bun.serve` with HTML-import routes: `/` (the app), `/mockups/` (gallery), `/mockups/NN-slug/`
`src/app/` -> the voyage app: `state.ts` (URL state), `controller.ts` + `voyage.ts` (scene changes),
`world/` (three.js world, landings, ribbons), `frieze/` (friezes, curtain), `panel/`, `timeline/`, `content.ts`
`src/data/album/` -> the app's data: 9 sagas, 40 songs, motifs (`// TODO(verify)` where unconfirmed)
`src/data/` (other files) -> the mockups' shapes and sample data; frozen
`src/shared/` -> credits, audio slot (direct MP3, empty state), YouTube link cards, LRC parser
`src/content/` -> `/content/*` routes over the mounted content folder: library.json, files (ranges), Homer books
`src/mockups/NN-slug/` -> one design direction each: `index.html`, `main.ts`, `style.css`, plus optional helper modules (e.g. `scene.ts`, `layout.ts`)
+48 -23
View File
@@ -1,29 +1,54 @@
# Design
## Status: visual direction open
No design system, palette or type scale is chosen yet. Seven mockups under `src/mockups/` each commit fully
to a different direction; the maintainer will pick one, or combine several, and this file then records the
chosen direction as tokens and rules. Until then, do not treat any single mockup's palette or fonts as the
house style.
## Status: direction chosen (2026-10-30)
The app at `/` (`src/app/`) is the house style. Its record, with every chosen and deselected idea and the
user's words: `docs/design/002-voyage-app.md`. The mockups under `src/mockups/` are history; don't take
styling from the dropped ones. New options vary structure, flow and behaviour within this direction —
never a new palette or font family.
## The candidate directions
| # | Folder | Direction |
## The direction: "two worlds" (look 2)
A stylised 3D sea voyage, bright and naturalistic in colour, framed by a Greek black-figure frieze that
drops like a curtain between scenes. The interface sits on parchment and borrows the frieze's terracotta,
black and meander band so that frieze, world and panels read as one piece.
## Tokens (`src/app/style.css` `:root`)
| Token | Value | Use |
|---|---|---|
| 1 | `01-editorial` | Editorial scroll: horizontal line, parchment and serif, motifs as thin threads |
| 2 | `02-vertical-story` | Vertical story: downward scroll with a sticky detail panel |
| 3 | `03-transit-map` | Transit map: motifs as metro lines, songs as stations |
| 4 | `04-greek-frieze` | Greek frieze: an unrolled black-figure vase frieze |
| 5 | `05-score` | Score / waveform: staff or waveform, parts as segments |
| 6 | `06-constellation` | Constellation (3D, three.js): songs as stars |
| 7 | `07-sea-voyage` | Sea voyage (3D, three.js): ship, sea, saga islands |
| `--clay` | `#c46a33` | frieze ground; accents, active tab, play button |
| `--clay-deep` | `#9a4520` | eyebrows, hover, small accent text |
| `--glaze` | `#1d140e` | frieze figures; rules, borders, dark buttons, page frame |
| `--added-white` | `#efe3c8` | text on glaze |
| `--vellum` / `--vellum-hi` / `--vellum-lo` | `#e3d6b4` / `#ece2c6` / `#e0d1ab` | panel, cards, timeline |
| `--iron` / `--iron-soft` | `#2b2118` / `#5a4836` | body text / secondary text |
| `--line` | `rgb(43 33 24 / 0.2)` | hairlines |
| `--meander` | SVG data URI | the key band: 14px strip between two 2px glaze rules (`.meander-edge`) on top of every panel, card and bar |
Details per direction: `docs/specs/001-timeline-mockups.md`.
Motif colours come from the data (`src/data/album/motifs.ts`) and are the same everywhere: timeline,
panel, ribbons and chips. On parchment, text in a motif colour is darkened to 4.5:1
(`src/app/timeline/ink.ts`); dots and knots keep the bright colour.
## Constraints that hold for every direction
- Fonts are self-hosted from `@fontsource/*` (installed: Cormorant Garamond, Inter); nothing from a CDN.
- three.js only where a direction is 3D.
- Motif colours come from the data (`Motif.color` in `src/data/sample.ts`), so threads match across views.
- The fan-credits footer (`src/shared/credits.css`) is present on every page; a direction may restyle it
but not remove or hide it.
- Avoid generic, templated "AI-looking" UI; a direction that is just another layout in different colours
has failed.
## Type
- **Inter** for all UI, labels and controls: 13px minimum (`--fs-label`), 14px small, 15px panel body,
weights 400–600, tracking ≤0.06em. Never small wide-tracked capitals (they were "very hard to read").
- **Cormorant Garamond** only for large display titles (`--fs-title` 2rem), lyric lines, Odyssey reading
text, and frieze inscriptions (≥19px on screen).
## Layout
- Desktop reference 1440×900: stage (3D canvas + frieze layer + caption card + bottom timeline) | right
panel 420px (collapsible to a 56px tab) | credits footer across the bottom.
- Timeline ≤190px tall by default; the current saga expanded with titles, the others compact.
- Below 900px: the panel becomes a bottom sheet; the timeline stays over the stage.
## 3D
Stylised, not realistic: toon/flat shading, ink outlines, flat colour-band water with drawn wave strokes,
a small flat sun, graphic silhouettes. Black-figure cut-outs (the giant, Athena) inside landings.
Pixel ratio ≤2; distant places are not drawn.
## Motion
Curtain drop ≈1.2s, parting lift ≈1.5s, doorway zoom ≈1.6s, fade ≈0.45/0.65s; ease-out curves.
`prefers-reduced-motion`: every scene change is a 200ms crossfade.
## Constraints
- Fonts self-hosted from `@fontsource/*`; three.js from npm; nothing from a CDN.
- The fan-credits footer is on every page and never hidden.
- Avoid generic, templated "AI-looking" UI.
+9 -7
View File
@@ -19,16 +19,18 @@ recur across the songs and how the story maps onto the *Odyssey*. Desktop first
(`src/shared/credits.ts`), never retyped.
- **Accuracy is visible:** content that isn't verified is marked as such (`// TODO(verify)` in data) and never
presented as fact-checked.
- **Lyrics:** mockups use placeholder lyric lines only. Whether and how real lyrics are shown is an open
copyright question, deferred by the maintainer.
- **Media:** videos are YouTube link cards (no embedded player); audio supports direct MP3 URLs. The app
works offline apart from following those links.
- **Lyrics and audio:** real recordings, lyrics and cover art come only from a content folder mounted at
runtime (spec 003); they are never committed or baked into the image. The broader copyright question is
deferred by the maintainer.
- **Media:** videos are YouTube link cards (no embedded player). The app works without the content folder
and offline apart from following those links.
## Tone
Polished and smooth; may use 3D (three.js). It should feel like a crafted piece made by a fan, not a generic
template — see `DESIGN.md`.
## Current stage
Spec `docs/specs/001-timeline-mockups.md`: 7 design-direction mockups over shared sample data (Troy and
Cyclops sagas plus later callback songs). The maintainer picks or combines a direction; the real app gets
its own spec after that.
v1 of the app (spec `docs/specs/002-voyage-app.md`): the whole album, 9 sagas and 40 songs, as a stylised
3D sea voyage with the frieze as a curtain between scenes, three landings, a side panel and a bottom
timeline of motif threads. The design rounds that led here (mockups 01–17) are recorded in
`docs/design/002-direction-round-notes.md`.
+25 -9
View File
@@ -3,10 +3,12 @@
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 an index page and seven mockup pages. 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. All pages read one typed sample dataset and mount one
shared credits snippet.
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 |
@@ -26,18 +28,30 @@ shared credits snippet.
| `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. |
| `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). |
| `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` | Runs steps one at a time, last request wins, a cancelled step's curtain is lifted by the next. Order under a curtain: cover → sail → landing → horizon on the frieze ground line → lift. Play voyage (3.4s per song, silent), 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; no runtime API).
3. `main.ts` renders the direction and calls `mountCredits()` into `<footer data-fan-credits>`.
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, Butler's
Odyssey and Iliad). It is mounted read-only, never committed or baked into the image (spec 003). Without it
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.
@@ -47,6 +61,8 @@ the site works and `/content/*` answers 404. Otherwise none. Fonts (`@fontsource
(`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`);
+15
View File
@@ -90,3 +90,18 @@ Roman names (Ulysses, Minerva, Jove); EPIC uses the Greek ones. Both are public
**Decision:** the album data has no Odysseus character theme. **Why:** character themes are derived from
singers, and Odysseus sings in 35 of 40 songs, so his thread marked almost every stop and crowded the
timeline ("+28 more"). His story is carried by the musical motifs (Warrior of the Mind, Just a Man, …).
## 2026-10-30 — The voyage app (spec 002): structure and v1 choices
- **Album data is separate from the mockups' data** (`src/data/album/` vs `src/data/types.ts`/`sample.ts`),
so the 17 mockups keep working unchanged.
- **App at `/`, gallery at `/mockups/`.** The mockups stay served as the design record.
- **Odyssey references are a book plus keywords**, not line numbers: the public-domain prose translations
have no line numbers. Links point at Butcher & Lang (Gutenberg #1728).
- **Landings:** cave = Polyphemus, Survive; arena = God Games; hall = all five Ithaca songs.
- **Transitions:** same saga sails, the adjacent saga drops its frieze, further jumps fade. Last request
wins; a curtain left down by a cancelled step is lifted by the next one.
- **Play voyage is silent** (3.4s per song): starting audio without a click is close to autoplay.
- **Fan works are empty in v1** with an empty state; no invented works are shown.
- **Without WebGL** the frieze is the stage (dropped, never lifted), so the album stays browsable.
- **Back to sea** leaves a landing but keeps the song.
+10
View File
@@ -88,3 +88,13 @@ Small, specific, independently actionable. Remove only when actually done.
- Homer books: with Butler's fallback text, the last book (Odyssey XXIV) also swallows the translator's
notes after it. The preferred Butcher & Lang text does not have this problem.
- App v1 (spec 002) follow-ups:
- 97 album values are `// TODO(verify)` (story years, some singers, Odyssey books/keywords, facts).
- Parts and motifs are not time-aligned to the recordings; needs a mapping file in the content folder
(part → first lyric line). Then the lyrics can show part headings and motif colours per line.
- Passage starts in the Odyssey reading view are keyword guesses.
- Fan works: decide sourcing, credit and permission process; the data type exists, the list is empty.
- Mobile (<900px) is usable but not polished; no favicon yet (one 404 per page load).
- Some frieze figures are small at five songs per saga; Charybdis reads mainly as a ring from the ship.
- Timeline motif inks on parchment can look alike (Warrior / Thunder Bringer / Zeus).