cosmo/frontend/src/components/CameraController.tsx

136 lines
4.3 KiB
TypeScript
Raw Normal View History

/**
* CameraController - handles camera movement and focus
*/
import { useEffect, useRef } from 'react';
import { useFrame, useThree } from '@react-three/fiber';
import { Vector3 } from 'three';
import type { CelestialBody } from '../types';
2025-11-29 15:10:00 +00:00
import { calculateRenderPosition, findParentPlanet } from '../utils/renderPosition';
interface CameraControllerProps {
focusTarget: CelestialBody | null;
2025-11-29 15:10:00 +00:00
allBodies: CelestialBody[];
onAnimationComplete?: () => void;
2025-11-30 02:43:47 +00:00
resetTrigger?: number;
}
2025-11-30 02:43:47 +00:00
export function CameraController({ focusTarget, allBodies, onAnimationComplete, resetTrigger = 0 }: CameraControllerProps) {
const { camera } = useThree();
const targetPosition = useRef(new Vector3());
const isAnimating = useRef(false);
const animationProgress = useRef(0);
const startPosition = useRef(new Vector3());
2025-11-30 02:43:47 +00:00
const lastResetTrigger = useRef(0);
2025-11-30 02:43:47 +00:00
// Handle manual reset trigger
useEffect(() => {
if (resetTrigger !== lastResetTrigger.current) {
lastResetTrigger.current = resetTrigger;
// Force reset
targetPosition.current.set(25, 20, 25);
startPosition.current.copy(camera.position);
isAnimating.current = true;
animationProgress.current = 0;
}
}, [resetTrigger, camera]); // Only run when resetTrigger changes
// Handle focus target changes
useEffect(() => {
if (focusTarget) {
2025-11-29 15:10:00 +00:00
// Focus on target - use smart rendered position
const renderPos = calculateRenderPosition(focusTarget, allBodies);
2025-11-30 02:43:47 +00:00
const currentTargetPos = new Vector3(renderPos.x, renderPos.z, renderPos.y);
2025-11-29 15:10:00 +00:00
2025-11-30 02:43:47 +00:00
// Calculate ideal camera position based on target
2025-11-29 16:58:58 +00:00
const pos = focusTarget.positions[0];
const distance = Math.sqrt(pos.x ** 2 + pos.y ** 2 + pos.z ** 2);
2025-11-29 15:10:00 +00:00
const parentInfo = findParentPlanet(focusTarget, allBodies);
let offset: number;
2025-11-30 02:43:47 +00:00
let heightMultiplier = 1;
let sideMultiplier = 1;
if (focusTarget.type === 'planet') {
offset = 4;
heightMultiplier = 1.5;
sideMultiplier = 1;
} else if (focusTarget.type === 'probe') {
2025-11-29 15:10:00 +00:00
if (parentInfo) {
2025-11-30 02:43:47 +00:00
offset = 3;
2025-11-29 15:10:00 +00:00
heightMultiplier = 0.8;
sideMultiplier = 1.2;
} else if (distance < 10) {
2025-11-30 02:43:47 +00:00
offset = 5;
2025-11-29 15:10:00 +00:00
heightMultiplier = 0.6;
sideMultiplier = 1.5;
} else if (distance > 50) {
2025-11-30 02:43:47 +00:00
offset = 4;
2025-11-29 15:10:00 +00:00
heightMultiplier = 0.8;
sideMultiplier = 1;
} else {
2025-11-30 02:43:47 +00:00
offset = 6;
2025-11-29 15:10:00 +00:00
heightMultiplier = 0.8;
sideMultiplier = 1.2;
}
} else {
offset = 10;
heightMultiplier = 1;
sideMultiplier = 1;
}
2025-11-29 16:58:58 +00:00
targetPosition.current.set(
currentTargetPos.x + (offset * sideMultiplier),
currentTargetPos.y + (offset * heightMultiplier),
currentTargetPos.z + offset
);
2025-11-30 02:43:47 +00:00
// Start animation to target
startPosition.current.copy(camera.position);
isAnimating.current = true;
animationProgress.current = 0;
2025-11-30 02:43:47 +00:00
} else {
2025-11-30 02:43:47 +00:00
// Target became null (e.g. info window closed)
// DO NOTHING here to preserve camera position
// Reset is handled by the other useEffect
// Just stop any ongoing animation
if (isAnimating.current) {
2025-11-29 16:58:58 +00:00
isAnimating.current = false;
animationProgress.current = 0;
}
}
2025-11-29 15:10:00 +00:00
}, [focusTarget, allBodies, camera]);
useFrame((_, delta) => {
if (isAnimating.current) {
// Smooth animation using easing
animationProgress.current += delta * 0.8; // Animation speed
if (animationProgress.current >= 1) {
animationProgress.current = 1;
isAnimating.current = false;
if (onAnimationComplete) onAnimationComplete();
}
// Easing function (ease-in-out)
const t = animationProgress.current;
const eased = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
// Interpolate camera position
camera.position.lerpVectors(startPosition.current, targetPosition.current, eased);
2025-11-29 15:10:00 +00:00
// Look at target - use smart rendered position (only during animation)
if (focusTarget) {
2025-11-29 15:10:00 +00:00
const renderPos = calculateRenderPosition(focusTarget, allBodies);
camera.lookAt(renderPos.x, renderPos.z, renderPos.y);
} else {
camera.lookAt(0, 0, 0);
}
}
// After animation completes, OrbitControls will take over
});
return null;
}