116 lines
3.9 KiB
TypeScript
116 lines
3.9 KiB
TypeScript
|
|
/**
|
||
|
|
* 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;
|
||
|
|
}
|