Files
GMW/services/frontend/src/lib/constellation/camera.ts
T

69 lines
1.6 KiB
TypeScript

/**
* Camera math for constellation fly-to — pure, easing-based interpolation.
*/
export interface CameraState {
/** Camera center in canvas space. */
x: number;
y: number;
/** Zoom factor (1 = fit). */
z: number;
}
export type Easing = (t: number) => number;
/** Smooth ease-in-out cubic. Monotonic on [0,1]. */
export const easeInOutCubic: Easing = (t) =>
t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
export function flyTo(
from: CameraState,
to: CameraState,
t: number,
ease: Easing = easeInOutCubic,
): CameraState {
const clamped = Math.max(0, Math.min(1, t));
const k = ease(clamped);
return {
x: from.x + (to.x - from.x) * k,
y: from.y + (to.y - from.y) * k,
z: from.z + (to.z - from.z) * k,
};
}
/** Fit-to-view camera so a scene always starts framed. */
export function fitCamera(
nodes: { x: number; y: number; r?: number }[],
width: number,
height: number,
padding = 80,
): CameraState {
if (nodes.length === 0 || width <= 0 || height <= 0) {
return { x: width / 2, y: height / 2, z: 1 };
}
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (const n of nodes) {
const r = n.r ?? 8;
minX = Math.min(minX, n.x - r);
maxX = Math.max(maxX, n.x + r);
minY = Math.min(minY, n.y - r);
maxY = Math.max(maxY, n.y + r);
}
const w = Math.max(1, maxX - minX);
const h = Math.max(1, maxY - minY);
const z = Math.min(
3,
Math.max(
0.35,
Math.min((width - padding * 2) / w, (height - padding * 2) / h),
),
);
return {
x: (minX + maxX) / 2,
y: (minY + maxY) / 2,
z,
};
}