feat(274): transect composition synced to the lyric
Song274Transect draws Route 274 in its true shape (the "Ground" view of song-274/route/274-transect.html) and plays it along with the song. - data.ts: reads song-274/route/ at load time (lyrics index, roster, route and the optional east/west stubs); nothing is copied into the code. Only sung nodes are shown; districts and 東に続く light nothing. - progress.ts: a "covered so far" paint over the dimmed road. Its head is pinned to each sung object's km as that line starts and glides between pins on a monotone cubic, so it never runs backwards. After 東に続く it runs out along the east stub; the west stub is always painted. - camera.ts: fixed close zoom during each narration part (lines under 20 s apart), following a Gaussian-smoothed head; pull back to frame the covered road plus some road ahead during breaks, and to the full map after the lyric. - nodeAnimation.ts: lightUp / switchOff as reusable functions over a NodeLook, with long lines switching off after MAX_LIT_S. - palette.ts: every colour as a named token (dark default, light alongside), including shrine and railway families. remotion.config.ts sets song-274/ as the public dir so staticFile() reaches the song's audio/ and route/ in place. Verified: eslint + tsc clean; stills checked across parts, breaks and the ending. Render with --muted and add audio with ffmpeg: Remotion's own AAC lands 42.75 ms late. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PLt25nCVMsoroKALYUBu6u
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
import type { RoutePoint } from "./data";
|
||||
|
||||
/**
|
||||
* The "Ground — true shape" projection of `274-transect.html`, in the page's
|
||||
* own units: an equirectangular plot with longitude shrunk by cos(lat), so
|
||||
* distances read true east–west and north–south. camera.ts decides how these
|
||||
* units land on screen.
|
||||
*/
|
||||
export const GROUND = {
|
||||
width: 1200,
|
||||
pad: 34,
|
||||
} as const;
|
||||
|
||||
export type Projection = {
|
||||
height: number;
|
||||
x: (lon: number) => number;
|
||||
y: (lat: number) => number;
|
||||
};
|
||||
|
||||
export const projectGround = (route: RoutePoint[]): Projection => {
|
||||
const lats = route.map((p) => p[1]);
|
||||
const lons = route.map((p) => p[2]);
|
||||
const la0 = Math.min(...lats);
|
||||
const la1 = Math.max(...lats);
|
||||
const lo0 = Math.min(...lons);
|
||||
const lo1 = Math.max(...lons);
|
||||
|
||||
const kx = Math.cos((((la0 + la1) / 2) * Math.PI) / 180);
|
||||
const sx = (lo1 - lo0) * kx;
|
||||
const sy = la1 - la0;
|
||||
const iw = GROUND.width - 2 * GROUND.pad;
|
||||
const ih = (iw * sy) / sx;
|
||||
|
||||
return {
|
||||
height: ih + 2 * GROUND.pad,
|
||||
x: (lon) => GROUND.pad + (((lon - lo0) * kx) / sx) * iw,
|
||||
y: (lat) => GROUND.pad + ih - ((lat - la0) / sy) * ih,
|
||||
};
|
||||
};
|
||||
|
||||
export type Bounds = { minX: number; minY: number; maxX: number; maxY: number };
|
||||
|
||||
/** Bounds of the projected route from its start up to each point, inclusive. */
|
||||
export const prefixBounds = (route: RoutePoint[], proj: Projection): Bounds[] => {
|
||||
const out: Bounds[] = [];
|
||||
for (const p of route) {
|
||||
const x = proj.x(p[2]);
|
||||
const y = proj.y(p[1]);
|
||||
const prev = out[out.length - 1];
|
||||
out.push(prev ? extendBounds(prev, x, y) : { minX: x, minY: y, maxX: x, maxY: y });
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
export const extendBounds = (b: Bounds, x: number, y: number): Bounds => ({
|
||||
minX: Math.min(b.minX, x),
|
||||
minY: Math.min(b.minY, y),
|
||||
maxX: Math.max(b.maxX, x),
|
||||
maxY: Math.max(b.maxY, y),
|
||||
});
|
||||
|
||||
export const routePath = (points: RoutePoint[], proj: Projection): string =>
|
||||
points
|
||||
.map(
|
||||
(p, i) =>
|
||||
`${i ? "L" : "M"}${proj.x(p[2]).toFixed(2)} ${proj.y(p[1]).toFixed(2)}`,
|
||||
)
|
||||
.join("");
|
||||
Reference in New Issue
Block a user