SOURCE / PINNED RELEASE
Made of little things.
Atlas map viewer
- Release
- 1d2807e80b44…
- Author-recorded commit
- 43810ed2b368…
- License
- LICENSE
- Author’s source reference
- nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-4757dbabc86
Archive hash verified: 8db6876d96c460ac…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import { project, unproject } from "../../format/src/index";
export type Point = [number, number];
export interface Camera {
center: Point;
zoom: number;
width: number;
height: number;
pitch: number;
bearing: number;
}
/** Orthographic camera, matching the WASM vertex shader. Bearing is clockwise from north. */
export function groundDelta(
x: number,
y: number,
pitch: number,
bearing: number,
): Point {
const r = (bearing * Math.PI) / 180,
c = Math.cos(r),
s = Math.sin(r);
y /= Math.cos((pitch * Math.PI) / 180);
return [c * x - s * y, s * x + c * y];
}
export function metersPerPixel(camera: Camera) {
const lat = unproject(...camera.center)[1];
return (
(Math.cos((lat * Math.PI) / 180) * 40075016.686) / (256 * 2 ** camera.zoom)
);
}
export function worldToScreen(
p: Point,
camera: Camera,
heightMeters = 0,
): Point {
const world = 256 * 2 ** camera.zoom,
r = (camera.bearing * Math.PI) / 180,
pitch = (camera.pitch * Math.PI) / 180,
dx = (p[0] - camera.center[0]) * world,
dy = (p[1] - camera.center[1]) * world;
return [
Math.cos(r) * dx + Math.sin(r) * dy + camera.width / 2,
(-Math.sin(r) * dx + Math.cos(r) * dy) * Math.cos(pitch) -
(heightMeters
? (heightMeters / metersPerPixel(camera)) * Math.sin(pitch)
: 0) +
camera.height / 2,
];
}
export function screenToWorld(p: Point, camera: Camera): Point {
const d = groundDelta(
p[0] - camera.width / 2,
p[1] - camera.height / 2,
camera.pitch,
camera.bearing,
),
world = 256 * 2 ** camera.zoom;
return [camera.center[0] + d[0] / world, camera.center[1] + d[1] / world];
}
export function projectScreen(p: Point, camera: Camera): Point {
return worldToScreen(project(...p), camera);
}
export function cameraBounds(
camera: Camera,
padding = 0,
): [number, number, number, number] {
const corners = [
[-padding, -padding],
[camera.width + padding, -padding],
[camera.width + padding, camera.height + padding],
[-padding, camera.height + padding],
].map((p) => screenToWorld(p as Point, camera));
return [
Math.min(...corners.map((p) => p[0])),
Math.min(...corners.map((p) => p[1])),
Math.max(...corners.map((p) => p[0])),
Math.max(...corners.map((p) => p[1])),
];
}
/** Fit all four ground corners after rotation and tilt, not just the north-up diagonal. */
export function fitZoom(
bounds: [number, number, number, number],
width: number,
height: number,
pitch: number,
bearing: number,
): number {
const [w, n, e, s] = bounds;
const camera: Camera = {
center: [(w + e) / 2, (n + s) / 2],
zoom: 0,
width: 0,
height: 0,
pitch,
bearing,
};
const points = [
[w, n],
[e, n],
[e, s],
[w, s],
].map((p) => worldToScreen(p as Point, camera));
const dx =
Math.max(...points.map((p) => p[0])) - Math.min(...points.map((p) => p[0]));
const dy =
Math.max(...points.map((p) => p[1])) - Math.min(...points.map((p) => p[1]));
return Math.log2(
Math.min(
Math.max(100, width) / Math.max(dx, 0.00001),
Math.max(100, height) / Math.max(dy, 0.00001),
),
);
}
/** Compute camera trigonometry once for a batch of ground-plane points. */
export function worldProjector(camera: Camera): (p: Point) => Point {
const world = 256 * 2 ** camera.zoom,
r = (camera.bearing * Math.PI) / 180,
c = Math.cos(r),
s = Math.sin(r),
pitch = Math.cos((camera.pitch * Math.PI) / 180);
const [cx, cy] = camera.center,
hw = camera.width / 2,
hh = camera.height / 2;
return (p) => {
const dx = (p[0] - cx) * world,
dy = (p[1] - cy) * world;
return [c * dx + s * dy + hw, (-s * dx + c * dy) * pitch + hh];
};
}
