cosmo/frontend/src/components/Stars.tsx

141 lines
3.9 KiB
TypeScript

/**
* Stars component - renders nearby stars in 3D space
*/
import { useEffect, useState, useMemo } from 'react';
import { Text, Billboard } from '@react-three/drei';
import * as THREE from 'three';
interface Star {
name: string;
name_zh: string;
distance_ly: number;
ra: number; // Right Ascension in degrees
dec: number; // Declination in degrees
magnitude: number;
color: string;
}
/**
* Convert RA/Dec to Cartesian coordinates
* RA: Right Ascension (0-360 degrees)
* Dec: Declination (-90 to 90 degrees)
* Distance: fixed distance for celestial sphere
*/
function raDecToCartesian(ra: number, dec: number, distance: number = 150) {
// Convert to radians
const raRad = (ra * Math.PI) / 180;
const decRad = (dec * Math.PI) / 180;
// Convert to Cartesian coordinates
const x = distance * Math.cos(decRad) * Math.cos(raRad);
const y = distance * Math.cos(decRad) * Math.sin(raRad);
const z = distance * Math.sin(decRad);
return new THREE.Vector3(x, y, z);
}
/**
* Scale star brightness based on magnitude
* Lower magnitude = brighter star
*/
function magnitudeToSize(magnitude: number): number {
// Brighter stars (lower magnitude) should be slightly larger
// But all stars should be very small compared to planets
const normalized = Math.max(-2, Math.min(12, magnitude));
return Math.max(0.15, 0.6 - normalized * 0.04);
}
export function Stars() {
const [stars, setStars] = useState<Star[]>([]);
useEffect(() => {
// Load star data from API
fetch('http://localhost:8000/api/celestial/static/star')
.then((res) => {
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
return res.json();
})
.then((data) => {
// API returns { category, items: [{ id, name, name_zh, data: {...} }] }
const starData = data.items.map((item: any) => ({
name: item.name,
name_zh: item.name_zh,
distance_ly: item.data.distance_ly,
ra: item.data.ra,
dec: item.data.dec,
magnitude: item.data.magnitude,
color: item.data.color,
}));
setStars(starData);
})
.catch((err) => console.error('Failed to load stars:', err));
}, []);
const starData = useMemo(() => {
return stars.map((star) => {
// Place all stars on a celestial sphere at fixed distance (150 units)
// This way they appear as background objects, similar to constellations
const position = raDecToCartesian(star.ra, star.dec, 150);
// Size based on brightness (magnitude)
const size = magnitudeToSize(star.magnitude);
return {
...star,
position,
size,
};
});
}, [stars]);
if (starData.length === 0) {
return null;
}
return (
<group>
{starData.map((star) => (
<group key={star.name}>
{/* Star sphere */}
<mesh position={star.position}>
<sphereGeometry args={[star.size, 16, 16]} />
<meshBasicMaterial
color={star.color}
transparent
opacity={0.9}
blending={THREE.AdditiveBlending}
/>
</mesh>
{/* Star glow */}
<mesh position={star.position}>
<sphereGeometry args={[star.size * 2, 16, 16]} />
<meshBasicMaterial
color={star.color}
transparent
opacity={0.2}
blending={THREE.AdditiveBlending}
/>
</mesh>
{/* Star name label - positioned radially outward from star */}
<Billboard position={star.position.clone().multiplyScalar(1.05)}>
<Text
fontSize={1.2}
color="#FFFFFF"
anchorX="center"
anchorY="middle"
outlineWidth={0.08}
outlineColor="#000000"
>
{star.name_zh}
</Text>
</Billboard>
</group>
))}
</group>
);
}