// Paths: the motif strip (only while the bottom timeline is hidden), then one row per motif of the song with // where it was heard before, where it plays here and where it returns, the followed motif's whole thread, // and the chart of the nine places. import { motifs, sagaOf, songs, type AlbumSong, type Motif } from "../../data/album"; import { chartSvg } from "./chart"; import { dot, esc, hitsOf, motifsOf } from "./util"; export function pathsView(song: AlbumSong, pick: string | null, showStrip: boolean): HTMLElement { const el = document.createElement("div"); el.className = "paths"; const ms = motifsOf(song); const followed = pick ? motifs.find((m) => m.id === pick) ?? null : null; el.innerHTML = ` ${showStrip ? motifStrip(song, ms) : ""}

Motif threads

${ms.length ? `

Each thread is a motif or a character's theme. Pick one to follow it through the album.

` : `

No motifs are recorded for this song yet.

`} ${followed ? threadDetail(song, followed) : ""}

The route

${chartSvg(song)}
${esc(sagaOf(song).place)} · stop ${song.track} of ${songs.length} on the voyage.
`; return el; } const songBtn = (s: AlbumSong) => ``; function motifRow(song: AlbumSong, m: Motif): string { const hits = hitsOf(m.id); const before = hits.filter((s) => s.track < song.track); const after = hits.filter((s) => s.track > song.track); const prev = before.at(-1); const next = after[0]; const inParts = song.parts.filter((p) => p.motifIds.includes(m.id)).map((p) => p.name); const here = inParts.length && inParts.length < song.parts.length ? `in ${inParts.map(esc).join(", ")}` : "throughout"; return `
  • Heard before
    ${prev ? songBtn(prev) : `first heard here`}
    Here
    ${here}
    Returns
    ${next ? songBtn(next) + (after.length > 1 ? ` +${after.length - 1} more` : "") : `not heard again`}
  • `; } /** The followed motif: what it is and every song it plays in. */ function threadDetail(song: AlbumSong, m: Motif): string { const hits = hitsOf(m.id); return `

    ${dot(m)}Following ${esc(m.name)} · ${hits.length} song${hits.length === 1 ? "" : "s"}

    ${esc(m.description)}

      ${hits.map((s) => `
    1. `).join("")}
    `; } /* The motif strip: the bottom timeline in miniature, for when the timeline is hidden. The album's 40 songs as stops on a course (saga dividers, the ship on the current song), then one thread per motif of this song, knotted where it plays. */ const W = 356, PAD = 8, COURSE_Y = 26, ROW = 34; const X = (i: number) => PAD + (i * (W - PAD * 2)) / (songs.length - 1); function motifStrip(song: AlbumSong, ms: Motif[]): string { const cur = songs.indexOf(song); const H = COURSE_Y + 14 + ms.length * ROW; let svg = ``; svg += ``; songs.forEach((s, i) => { if (i > 0 && s.sagaId !== songs[i - 1]!.sagaId) { const x = ((X(i) + X(i - 1)) / 2).toFixed(1); svg += ``; } svg += `${s.track}. ${esc(s.title)}`; }); svg += ``; ms.forEach((m, k) => { const y = COURSE_Y + 14 + k * ROW + ROW - 9; const idx = hitsOf(m.id).map((s) => songs.indexOf(s)); const x0 = X(idx[0]!), x1 = X(idx.at(-1)!); svg += ` ${idx.map((i) => `${esc(m.name)} in ${esc(songs[i]!.title)}`).join("")} `; }); const names = ms.map((m, k) => ``).join(""); return `

    Motif pathsTrack ${song.track} of ${songs.length} · heard before · here · returns

    ${names}
    `; }