Back to Atlas map viewer
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.

packages/renderer/src/camera.ts
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];
  };
}