59 lines
1.7 KiB
TypeScript
59 lines
1.7 KiB
TypeScript
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<Camera>(() => 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,
|
|
};
|
|
}
|