cosmo/frontend/src/components/Stars.tsx

145 lines
4.2 KiB
TypeScript
Raw Normal View History

2025-11-27 10:14:25 +00:00
/**
* Stars component - renders nearby stars in 3D space
*/
import { useEffect, useState, useMemo } from 'react';
import { Billboard } from '@react-three/drei';
2025-11-27 10:14:25 +00:00
import * as THREE from 'three';
import { request } from '../utils/request';
import { createLabelTexture } from '../utils/labelTexture';
2025-11-27 10:14:25 +00:00
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;
position: THREE.Vector3;
size: number;
2025-11-27 10:14:25 +00:00
}
/**
* Convert RA/Dec to Cartesian coordinates
* RA: Right Ascension (0-360 degrees)
* Dec: Declination (-90 to 90 degrees)
* Distance: fixed distance for celestial sphere
*/
2025-11-29 16:58:58 +00:00
function raDecToCartesian(ra: number, dec: number, distance: number = 5000) {
2025-11-27 10:14:25 +00:00
// 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));
2025-11-29 16:58:58 +00:00
return Math.max(5, 20 - normalized * 1.2);
2025-11-27 10:14:25 +00:00
}
// Sub-component for individual star to handle label texture efficiently
function StarObject({ star, geometry }: { star: Star; geometry: THREE.SphereGeometry }) {
// Generate label texture
const labelTexture = useMemo(() => {
return createLabelTexture(star.name_zh, null, "", "#FFFFFF");
}, [star.name_zh]);
return (
<group>
{/* Star sphere */}
<mesh position={star.position} geometry={geometry} 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={geometry} 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 */}
{labelTexture && (
<Billboard position={star.position.clone().multiplyScalar(1.05)}>
<mesh scale={[200, 100, 1]}>
<planeGeometry />
<meshBasicMaterial
map={labelTexture}
transparent
depthWrite={false}
toneMapped={false}
/>
</mesh>
</Billboard>
)}
</group>
);
}
2025-11-27 10:14:25 +00:00
export function Stars() {
const [stars, setStars] = useState<Star[]>([]);
useEffect(() => {
2025-11-29 15:10:00 +00:00
// Load star data from API
request.get('/celestial/static/star')
2025-11-29 15:10:00 +00:00
.then((res) => {
const data = res.data;
2025-11-29 15:10:00 +00:00
// API returns { category, items: [{ id, name, name_zh, data: {...} }] }
const starData = data.items.map((item: any) => {
// Place all stars on a celestial sphere at fixed distance (5000 units)
const position = raDecToCartesian(item.data.ra, item.data.dec, 5000);
const size = magnitudeToSize(item.data.magnitude);
return {
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,
position,
size
};
});
2025-11-29 15:10:00 +00:00
setStars(starData);
})
2025-11-27 10:14:25 +00:00
.catch((err) => console.error('Failed to load stars:', err));
}, []);
// Reuse geometry for all stars to improve performance
const sphereGeometry = useMemo(() => new THREE.SphereGeometry(1, 16, 16), []);
if (stars.length === 0) {
2025-11-27 10:14:25 +00:00
return null;
}
return (
<group>
{stars.map((star) => (
<StarObject key={star.name} star={star} geometry={sphereGeometry} />
2025-11-27 10:14:25 +00:00
))}
</group>
);
}