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:
1 parent
52feed514d
commit
7503f55a17
5 files changed
+357
No files matched your search
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user