Files
star-map/src/app/features/galaxy-system/camera-rig-controller.ts
T
Senrokai d7e8ea1d4d @
Add star-map Angular app, ETL pipeline, and caveman plugin

Angular 3D star map (galaxy/system/body views, Three.js rendering,
navigation store) plus the NASA ETL tooling that builds the star,
exoplanet and solar-system datasets, Playwright e2e suite, and the
cs:caveman Claude Code plugin (command, agent, skill).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@
2026-08-03 16:50:10 +02:00

82 lines
2.6 KiB
TypeScript

import * as THREE from 'three/webgpu';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
export interface CameraPose {
position: THREE.Vector3;
target: THREE.Vector3;
}
function easeInOutCubic(t: number): number {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}
/**
* Drives smooth camera-position/look-at tweens between two poses. Deliberately unaware of
* parsecs vs. AU: the galaxy-to-system transition is built from two of these tweens (one per
* unit space) with a "floating-origin" recenter — an instantaneous {@link setImmediate} jump
* that swaps which group is visible and which unit scale the camera/controls operate in —
* spliced in between them by the caller (`GalaxySystemSceneComponent`).
*/
export class CameraRigController {
private active?: {
from: CameraPose;
to: CameraPose;
duration: number;
elapsed: number;
onComplete?: () => void;
};
constructor(
private readonly camera: THREE.PerspectiveCamera,
private readonly controls: OrbitControls
) {}
get isAnimating(): boolean {
return !!this.active;
}
/** Starts (replacing any in-flight tween) an eased camera-pose animation. */
flyTo(to: CameraPose, durationSeconds: number, onComplete?: () => void): void {
this.controls.enabled = false;
this.active = {
from: { position: this.camera.position.clone(), target: this.controls.target.clone() },
to: { position: to.position.clone(), target: to.target.clone() },
duration: Math.max(durationSeconds, 0.001),
elapsed: 0,
onComplete
};
}
/**
* Instantly places the camera/controls-target with no animation — the floating-origin
* recenter jump performed right after swapping which unit space/group is visible.
*/
setImmediate(pose: CameraPose): void {
this.camera.position.copy(pose.position);
this.controls.target.copy(pose.target);
this.camera.lookAt(pose.target);
}
/** Advances any in-flight tween. Call once per rendered frame. */
update(deltaSeconds: number): void {
if (!this.active) {
return;
}
this.active.elapsed += deltaSeconds;
const t = Math.min(this.active.elapsed / this.active.duration, 1);
const eased = easeInOutCubic(t);
this.camera.position.lerpVectors(this.active.from.position, this.active.to.position, eased);
this.controls.target.lerpVectors(this.active.from.target, this.active.to.target, eased);
this.camera.lookAt(this.controls.target);
if (t >= 1) {
const { onComplete } = this.active;
this.active = undefined;
this.controls.enabled = true;
onComplete?.();
}
}
}