/** * 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'; import { scalePosition, scaleDistance } from '../utils/scaleDistance'; interface CameraControllerProps { focusTarget: CelestialBody | null; onAnimationComplete?: () => void; } export function CameraController({ focusTarget, onAnimationComplete }: CameraControllerProps) { const { camera } = useThree(); const targetPosition = useRef(new Vector3()); const isAnimating = useRef(false); const animationProgress = useRef(0); const startPosition = useRef(new Vector3()); useEffect(() => { if (focusTarget) { // Focus on target - use scaled position const pos = focusTarget.positions[0]; const scaledPos = scalePosition(pos.x, pos.y, pos.z); const distance = Math.sqrt(pos.x ** 2 + pos.y ** 2 + pos.z ** 2); const scaledDistance = scaleDistance(distance); // Calculate camera position based on target type let offset: number; let heightMultiplier = 1; // For adjusting vertical position let sideMultiplier = 1; // For adjusting horizontal offset if (focusTarget.type === 'planet') { // For planets, use closer view from above offset = 4; heightMultiplier = 1.5; sideMultiplier = 1; } else if (focusTarget.type === 'probe') { // For probes, determine view based on actual distance from Sun if (distance < 10) { // Very close probes (inner solar system, like Juno near Jupiter, Parker near Sun) // Use a wide-angle side view to show both probe and nearby planet offset = 15; heightMultiplier = 0.4; // Lower camera for better side view sideMultiplier = 2; // Move camera to the side } else if (scaledDistance > 50) { // Far probes (Voyagers, New Horizons) offset = 20; heightMultiplier = 1; sideMultiplier = 1; } else { // Medium distance probes offset = 8; heightMultiplier = 1; sideMultiplier = 1; } } else { offset = 10; heightMultiplier = 1; sideMultiplier = 1; } targetPosition.current.set( scaledPos.x + (offset * sideMultiplier), scaledPos.z + (offset * heightMultiplier), scaledPos.y + offset ); // Start animation startPosition.current.copy(camera.position); isAnimating.current = true; animationProgress.current = 0; } else { // Return to solar system overview (angled view) targetPosition.current.set(50, 40, 50); startPosition.current.copy(camera.position); isAnimating.current = true; animationProgress.current = 0; } }, [focusTarget, 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); // Look at target - use scaled position (only during animation) if (focusTarget) { const pos = focusTarget.positions[0]; const scaledPos = scalePosition(pos.x, pos.y, pos.z); camera.lookAt(scaledPos.x, scaledPos.z, scaledPos.y); } else { camera.lookAt(0, 0, 0); } } // After animation completes, OrbitControls will take over }); return null; }