202 lines
6.4 KiB
TypeScript
202 lines
6.4 KiB
TypeScript
/**
|
|
* Galaxies component - renders distant galaxies as billboards
|
|
*/
|
|
import { useEffect, useState, useMemo } from 'react';
|
|
import { Billboard, Text, useTexture } from '@react-three/drei';
|
|
import * as THREE from 'three';
|
|
import { fetchStaticData } from '../utils/api';
|
|
|
|
interface Galaxy {
|
|
name: string;
|
|
name_zh: string;
|
|
type: string;
|
|
distance_mly: number; // Distance in millions of light years
|
|
ra: number; // Right Ascension in degrees
|
|
dec: number; // Declination in degrees
|
|
magnitude: number;
|
|
diameter_kly: number; // Diameter in thousands of light years
|
|
color: string;
|
|
}
|
|
|
|
/**
|
|
* Create a procedural galaxy texture
|
|
*/
|
|
function createGalaxyTexture(color: string, type: string): THREE.Texture {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = 256;
|
|
canvas.height = 256;
|
|
const ctx = canvas.getContext('2d')!;
|
|
|
|
const centerX = canvas.width / 2;
|
|
const centerY = canvas.height / 2;
|
|
const radius = canvas.width / 2;
|
|
|
|
// Create radial gradient for galaxy glow
|
|
const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius);
|
|
|
|
// Parse color
|
|
const tempColor = new THREE.Color(color);
|
|
const r = Math.floor(tempColor.r * 255);
|
|
const g = Math.floor(tempColor.g * 255);
|
|
const b = Math.floor(tempColor.b * 255);
|
|
|
|
if (type === 'spiral') {
|
|
// Spiral galaxy: bright core with arms
|
|
gradient.addColorStop(0, `rgba(255, 255, 255, 1.0)`);
|
|
gradient.addColorStop(0.1, `rgba(${r}, ${g}, ${b}, 0.9)`);
|
|
gradient.addColorStop(0.3, `rgba(${r}, ${g}, ${b}, 0.6)`);
|
|
gradient.addColorStop(0.6, `rgba(${r}, ${g}, ${b}, 0.3)`);
|
|
gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`);
|
|
} else if (type === 'irregular') {
|
|
// Irregular galaxy: more diffuse
|
|
gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 0.8)`);
|
|
gradient.addColorStop(0.4, `rgba(${r}, ${g}, ${b}, 0.5)`);
|
|
gradient.addColorStop(0.8, `rgba(${r}, ${g}, ${b}, 0.2)`);
|
|
gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`);
|
|
} else {
|
|
// Galactic center or elliptical: bright concentrated core
|
|
gradient.addColorStop(0, `rgba(255, 255, 220, 1.0)`);
|
|
gradient.addColorStop(0.2, `rgba(${r}, ${g}, ${b}, 0.9)`);
|
|
gradient.addColorStop(0.5, `rgba(${r}, ${g}, ${b}, 0.5)`);
|
|
gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`);
|
|
}
|
|
|
|
ctx.fillStyle = gradient;
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Add some star-like points for detail (only for spiral galaxies)
|
|
if (type === 'spiral') {
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
|
|
for (let i = 0; i < 50; i++) {
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const dist = Math.random() * radius * 0.7;
|
|
const x = centerX + Math.cos(angle) * dist;
|
|
const y = centerY + Math.sin(angle) * dist;
|
|
const size = Math.random() * 1.5;
|
|
ctx.fillRect(x, y, size, size);
|
|
}
|
|
}
|
|
|
|
const texture = new THREE.CanvasTexture(canvas);
|
|
texture.needsUpdate = true;
|
|
return texture;
|
|
}
|
|
|
|
/**
|
|
* Convert RA/Dec to Cartesian coordinates for distant objects
|
|
*/
|
|
function raDecToCartesian(ra: number, dec: number, distance: number) {
|
|
const raRad = (ra * Math.PI) / 180;
|
|
const decRad = (dec * Math.PI) / 180;
|
|
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Calculate visual size based on actual diameter and distance
|
|
*/
|
|
function calculateAngularSize(diameterKly: number, distanceMly: number): number {
|
|
// Angular diameter in radians
|
|
const angularDiameter = diameterKly / (distanceMly * 1000);
|
|
// Significantly increased multiplier for larger distance
|
|
return Math.max(20, angularDiameter * 8000);
|
|
}
|
|
|
|
export function Galaxies() {
|
|
const [galaxies, setGalaxies] = useState<Galaxy[]>([]);
|
|
|
|
useEffect(() => {
|
|
// Load galaxy data from API
|
|
fetchStaticData('galaxy')
|
|
.then((response) => {
|
|
// Convert API response to Galaxy format
|
|
const galaxyData = response.items.map((item) => ({
|
|
name: item.name,
|
|
name_zh: item.name_zh,
|
|
type: item.data.type,
|
|
distance_mly: item.data.distance_mly,
|
|
ra: item.data.ra,
|
|
dec: item.data.dec,
|
|
magnitude: item.data.magnitude,
|
|
diameter_kly: item.data.diameter_kly,
|
|
color: item.data.color,
|
|
}));
|
|
setGalaxies(galaxyData);
|
|
})
|
|
.catch((err) => console.error('Failed to load galaxies:', err));
|
|
}, []);
|
|
|
|
const galaxyData = useMemo(() => {
|
|
return galaxies.map((galaxy) => {
|
|
// Place galaxies on celestial sphere at fixed distance for visualization
|
|
const visualDistance = 5000; // Fixed distance for celestial sphere (was 200)
|
|
const position = raDecToCartesian(galaxy.ra, galaxy.dec, visualDistance);
|
|
|
|
// Calculate visual size based on actual properties
|
|
const size = galaxy.type === 'galactic_center'
|
|
? 80 // Much larger for Milky Way center
|
|
: calculateAngularSize(galaxy.diameter_kly, galaxy.distance_mly);
|
|
|
|
// Create procedural texture for this galaxy
|
|
const texture = createGalaxyTexture(galaxy.color, galaxy.type);
|
|
|
|
return {
|
|
...galaxy,
|
|
position,
|
|
size,
|
|
texture,
|
|
};
|
|
});
|
|
}, [galaxies]);
|
|
|
|
if (galaxyData.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<group>
|
|
{galaxyData.map((galaxy) => (
|
|
<group key={galaxy.name}>
|
|
<Billboard
|
|
position={galaxy.position}
|
|
follow={true}
|
|
lockX={false}
|
|
lockY={false}
|
|
lockZ={false}
|
|
>
|
|
{/* Galaxy texture */}
|
|
<mesh>
|
|
<planeGeometry args={[galaxy.size * 3, galaxy.size * 3]} />
|
|
<meshBasicMaterial
|
|
map={galaxy.texture}
|
|
transparent
|
|
opacity={0.8}
|
|
blending={THREE.AdditiveBlending}
|
|
depthWrite={false}
|
|
/>
|
|
</mesh>
|
|
</Billboard>
|
|
|
|
{/* Galaxy name label - positioned slightly outward from galaxy */}
|
|
<Billboard position={galaxy.position.clone().multiplyScalar(1.03)}>
|
|
<Text
|
|
fontSize={60} // Increased from 1.5
|
|
color="#DDAAFF"
|
|
anchorX="center"
|
|
anchorY="middle"
|
|
outlineWidth={2}
|
|
outlineColor="#000000"
|
|
>
|
|
{galaxy.name_zh}
|
|
</Text>
|
|
</Billboard>
|
|
</group>
|
|
))}
|
|
</group>
|
|
);
|
|
}
|