export type Viewport = { zoom: number; pan: { x: number; y: number } }; export type ViewBounds = { x: number; y: number; width: number; height: number }; /** 境界を余白付きで中央へ収める。座標・倍率だけを扱う純粋関数。 */ export function fitViewport( bounds: ViewBounds, width: number, height: number, padding = 40, minZoom = 8, maxZoom = 160, ): Viewport { const zoom = Math.max( minZoom, Math.min( maxZoom, Math.min( (width - 2 * padding) / Math.max(bounds.width, 1), (height - 2 * padding) / Math.max(bounds.height, 1), ), ), ); return { zoom, pan: { x: (width - (2 * bounds.x + bounds.width) * zoom) / 2, y: (height - (2 * bounds.y + bounds.height) * zoom) / 2, }, }; } /** 指定した画面座標の下にある点を保ったまま倍率を変える。 */ export function zoomAround( point: { x: number; y: number }, viewport: Viewport, factor: number, minZoom = 5, maxZoom = 200, ): Viewport { const zoom = Math.max(minZoom, Math.min(maxZoom, viewport.zoom * factor)); const world = { x: (point.x - viewport.pan.x) / viewport.zoom, y: (point.y - viewport.pan.y) / viewport.zoom, }; return { zoom, pan: { x: point.x - world.x * zoom, y: point.y - world.y * zoom } }; }