wip(mockups): pause sea-chart round — saved spike plans and 08 geo module

Agents paused at the usage limit. Resume notes live in
docs/design/002-direction-round-notes.md and docs/design/002-sea-chart-spikes/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-23 19:30:13 +02:00
1 parent 52feed514d
commit 7503f55a17
5 files changed
+357

No files matched your search

+103
View File
@@ -0,0 +1,103 @@
import { songs } from "../../data/sample";
import { gapYears } from "../../data/query";
/** Chart world in SVG units. Geography is stylised; nothing is to scale. */
export const W = 1600;
export const H = 600;
export type Pt = readonly [number, number];
/** Closed Catmull-Rom outline through the points, for coastlines. */
export function closedSmooth(pts: Pt[]): string {
const n = pts.length;
let d = `M${pts[0]![0]},${pts[0]![1]}`;
for (let i = 0; i < n; i++) {
const p0 = pts[(i - 1 + n) % n]!, p1 = pts[i]!, p2 = pts[(i + 1) % n]!, p3 = pts[(i + 2) % n]!;
d += cubic(p0, p1, p2, p3);
}
return d + "Z";
}
function cubic(p0: Pt, p1: Pt, p2: Pt, p3: Pt): string {
const t = 1 / 6;
const c1: Pt = [p1[0] + (p2[0] - p0[0]) * t, p1[1] + (p2[1] - p0[1]) * t];
const c2: Pt = [p2[0] - (p3[0] - p1[0]) * t, p2[1] - (p3[1] - p1[1]) * t];
return ` C${c1[0].toFixed(1)},${c1[1].toFixed(1)} ${c2[0].toFixed(1)},${c2[1].toFixed(1)} ${p2[0]},${p2[1]}`;
}
export const land = {
troy: closedSmooth([[-40, -40], [170, -30], [186, 60], [160, 130], [205, 192], [232, 258], [214, 322], [246, 384], [222, 452], [178, 520], [196, 600], [120, 660], [-40, 660]]),
cicones: closedSmooth([[300, -40], [660, -40], [622, 38], [562, 78], [484, 92], [420, 70], [362, 84], [312, 40]]),
cyclops: closedSmooth([[800, 300], [842, 256], [905, 246], [972, 254], [1020, 298], [1030, 360], [990, 414], [920, 430], [852, 416], [806, 372]]),
goat: closedSmooth([[1078, 168], [1104, 160], [1122, 176], [1110, 196], [1084, 194]]),
rock: closedSmooth([[1508, 470], [1532, 462], [1546, 480], [1526, 494]]),
};
export interface PortGeo { at: Pt; label: [number, number, "start" | "middle" | "end"]; via?: Pt[] }
/** Port positions per song id, label offset, and waypoints sailed *before* arriving. */
export const ports: Record<string, PortGeo> = {
"the-horse-and-the-infant": { at: [258, 296], label: [18, 5, "start"] },
"just-a-man": { at: [300, 412], label: [18, 5, "start"] },
"full-speed-ahead": { at: [420, 134], label: [0, 34, "middle"], via: [[340, 270]] },
"open-arms": { at: [548, 142], label: [18, 5, "start"] },
"warrior-of-the-mind": { at: [652, 302], label: [0, 34, "middle"], via: [[612, 214]] },
polyphemus: { at: [768, 262], label: [-16, -14, "end"] },
survive: { at: [905, 220], label: [0, -20, "middle"], via: [[830, 222]] },
"remember-them": { at: [1052, 290], label: [18, 5, "start"], via: [[985, 226]] },
"my-goodbye": { at: [930, 458], label: [0, 34, "middle"], via: [[1068, 384], [1012, 452]] },
storm: { at: [1400, 320], label: [0, 44, "middle"], via: [[1100, 476], [1262, 408]] },
};
export interface RouteSeg { from: number; to: number; d: string; years: number }
/** One smooth course through every port and waypoint, split into port-to-port legs. */
export function buildRoute(): RouteSeg[] {
const pts: { p: Pt; port: number }[] = [];
songs.forEach((s, i) => {
const g = ports[s.id]!;
for (const v of g.via ?? []) pts.push({ p: v, port: -1 });
pts.push({ p: g.at, port: i });
});
const segs: RouteSeg[] = [];
let cur = "";
let from = 0;
for (let k = 0; k < pts.length - 1; k++) {
const p0 = pts[Math.max(0, k - 1)]!.p, p1 = pts[k]!.p, p2 = pts[k + 1]!.p, p3 = pts[Math.min(pts.length - 1, k + 2)]!.p;
if (!cur) cur = `M${p1[0]},${p1[1]}`;
cur += cubic(p0, p1, p2, p3);
const next = pts[k + 1]!;
if (next.port >= 0) {
segs.push({ from, to: next.port, d: cur, years: gapYears(songs[from]!, songs[next.port]!) });
from = next.port;
cur = "";
}
}
return segs;
}
/** Log line x positions: a short hop between songs, plus a fixed width per in-story year. */
export const LOG = { start: 70, hop: 156, year: 300, end: 260 };
export const logX: number[] = [];
songs.forEach((s, i) => {
if (i === 0) logX.push(LOG.start);
else logX.push(logX[i - 1]! + LOG.hop + Math.max(0, gapYears(songs[i - 1]!, s)) * LOG.year);
});
export const LOG_W = logX[logX.length - 1]! + LOG.end;
/** Log x → fractional song index (piecewise linear between song columns). */
export function xToIndex(x: number): number {
if (x <= logX[0]!) return 0;
for (let i = 0; i < logX.length - 1; i++) {
const a = logX[i]!, b = logX[i + 1]!;
if (x <= b) return i + (x - a) / (b - a);
}
return logX.length - 1;
}
export const yearsLabel = (y: number) => `${y} ${y === 1 ? "year" : "years"} at sea`;
/** Small deterministic RNG for the depth soundings. */
export function rng(seed: number) {
return () => ((seed = (seed * 16807) % 2147483647) - 1) / 2147483646;
}