import { create } from "zustand"; import type { MapPoint } from "./projection"; /** Pan (pixels) and zoom (1 = whole system fits) of the map view. */ export interface Camera { zoom: number; panX: number; panY: number; } export interface Viewport { width: number; height: number; } export const MIN_ZOOM = 0.5; export const MAX_ZOOM = 40; /** Leave a little room around the map edge at zoom 1. */ const FIT = 0.92; export const DEFAULT_CAMERA: Camera = { zoom: 1, panX: 0, panY: 0 }; export const useCamera = create(() => DEFAULT_CAMERA); /** Pixels per map unit at this zoom. */ function pixelsPerUnit(viewport: Viewport, camera: Camera): number { return (Math.min(viewport.width, viewport.height) / 2) * FIT * camera.zoom; } export function toScreen(point: MapPoint, viewport: Viewport, camera: Camera): MapPoint { const k = pixelsPerUnit(viewport, camera); return { x: viewport.width / 2 + camera.panX + point.x * k, y: viewport.height / 2 + camera.panY + point.y * k, }; } export function pan(camera: Camera, dx: number, dy: number): Camera { return { ...camera, panX: camera.panX + dx, panY: camera.panY + dy }; } /** Zooms by `factor`, keeping the map point under `anchor` (screen pixels) in place. */ export function zoomAt( camera: Camera, factor: number, anchor: MapPoint, viewport: Viewport, ): Camera { const zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, camera.zoom * factor)); const ratio = zoom / camera.zoom; const cx = viewport.width / 2; const cy = viewport.height / 2; return { zoom, panX: anchor.x - cx - (anchor.x - cx - camera.panX) * ratio, panY: anchor.y - cy - (anchor.y - cy - camera.panY) * ratio, }; }