cosmo/frontend/src/components/Stars.tsx

138 lines
4.0 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';
import { request } from '../utils/request';
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 = 5000) {
// 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(5, 20 - normalized * 1.2);
}
export function Stars() {
const [stars, setStars] = useState<Star[]>([]);
useEffect(() => {
// Load star data from API
request.get('/celestial/static/star')
.then((res) => {
const data = res.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 (5000 units)
// This way they appear as background objects, similar to constellations
const position = raDecToCartesian(star.ra, star.dec, 5000);
// Size based on brightness (magnitude)
const size = magnitudeToSize(star.magnitude);
return {
...star,
position,
size,
};
});
}, [stars]);
// Reuse geometry for all stars to improve performance
const sphereGeometry = useMemo(() => new THREE.SphereGeometry(1, 16, 16), []);
if (starData.length === 0) {
return null;
}
return (
<group>
{starData.map((star) => (
<group key={star.name}>
{/* Star sphere */}
<mesh position={star.position} geometry={sphereGeometry} scale={[star.size, star.size, star.size]}>
<meshBasicMaterial
color={star.color}
transparent
opacity={0.9}
blending={THREE.AdditiveBlending}
/>
</mesh>
{/* Star glow */}
<mesh position={star.position} geometry={sphereGeometry} scale={[star.size * 2, star.size * 2, star.size * 2]}>
<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={40} // Increased font size
color="#FFFFFF"
anchorX="center"
anchorY="middle"
outlineWidth={2}
outlineColor="#000000"
>
{star.name_zh}
</Text>
</Billboard>
</group>
))}
</group>
);
}