Files
ncmr-songs-videos/src/song-274/layout.ts
T

69 lines
2.0 KiB
TypeScript
Raw Normal View History

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("");