386 lines
12 KiB
TypeScript
386 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import React, { useRef, useMemo, useState } from "react";
|
|
import { Canvas, useFrame } from "@react-three/fiber";
|
|
import * as THREE from "three";
|
|
|
|
// 1. Central Star (Helios / Core Sun) with radiant corona
|
|
function CentralStar() {
|
|
const sunRef = useRef<THREE.Mesh>(null!);
|
|
const coronaRef = useRef<THREE.Mesh>(null!);
|
|
const flareRef = useRef<THREE.Mesh>(null!);
|
|
|
|
useFrame((state, delta) => {
|
|
const t = state.clock.getElapsedTime();
|
|
if (sunRef.current) {
|
|
sunRef.current.rotation.y += delta * 0.2;
|
|
const pulse = 1 + Math.sin(t * 2.5) * 0.03;
|
|
sunRef.current.scale.set(pulse, pulse, pulse);
|
|
}
|
|
if (coronaRef.current) {
|
|
coronaRef.current.rotation.y -= delta * 0.15;
|
|
coronaRef.current.rotation.z += delta * 0.08;
|
|
const pulse2 = 1.2 + Math.cos(t * 1.8) * 0.05;
|
|
coronaRef.current.scale.set(pulse2, pulse2, pulse2);
|
|
}
|
|
if (flareRef.current) {
|
|
flareRef.current.rotation.z -= delta * 0.2;
|
|
}
|
|
});
|
|
|
|
return (
|
|
<group>
|
|
{/* Intense Core Sun */}
|
|
<mesh ref={sunRef}>
|
|
<sphereGeometry args={[0.75, 32, 32]} />
|
|
<meshStandardMaterial
|
|
color="#38bdf8"
|
|
emissive="#60a5fa"
|
|
emissiveIntensity={2.5}
|
|
roughness={0.2}
|
|
/>
|
|
</mesh>
|
|
|
|
{/* Atmospheric Corona Layer */}
|
|
<mesh ref={coronaRef}>
|
|
<sphereGeometry args={[0.95, 24, 24]} />
|
|
<meshBasicMaterial
|
|
color="#818cf8"
|
|
transparent
|
|
opacity={0.3}
|
|
wireframe
|
|
/>
|
|
</mesh>
|
|
|
|
{/* Solar Flare Halo Disk */}
|
|
<mesh ref={flareRef} rotation={[Math.PI / 2, 0, 0]}>
|
|
<ringGeometry args={[0.9, 1.45, 32]} />
|
|
<meshBasicMaterial
|
|
color="#38bdf8"
|
|
transparent
|
|
opacity={0.2}
|
|
side={THREE.DoubleSide}
|
|
/>
|
|
</mesh>
|
|
|
|
{/* Radiant Solar Lights */}
|
|
<pointLight color="#67e8f9" intensity={4} distance={20} decay={2} />
|
|
<pointLight color="#c084fc" intensity={2.5} distance={15} position={[0, 1, 0]} decay={2} />
|
|
</group>
|
|
);
|
|
}
|
|
|
|
// 2. Orbital Path Ring (crisp, faint geometric trajectory)
|
|
function OrbitTrack({ radius, color = "#475569" }: { radius: number; color?: string }) {
|
|
const lineGeometry = useMemo(() => {
|
|
const points: THREE.Vector3[] = [];
|
|
const segments = 120;
|
|
for (let i = 0; i <= segments; i++) {
|
|
const theta = (i / segments) * Math.PI * 2;
|
|
points.push(new THREE.Vector3(Math.cos(theta) * radius, 0, Math.sin(theta) * radius));
|
|
}
|
|
return new THREE.BufferGeometry().setFromPoints(points);
|
|
}, [radius]);
|
|
|
|
return (
|
|
<primitive object={new THREE.Line(
|
|
lineGeometry,
|
|
new THREE.LineBasicMaterial({ color, transparent: true, opacity: 0.3 })
|
|
)} />
|
|
);
|
|
}
|
|
|
|
// 3. Orbiting Planet with optional Rings & Moons
|
|
interface PlanetProps {
|
|
name: string;
|
|
radius: number;
|
|
size: number;
|
|
speed: number;
|
|
color: string;
|
|
emissive?: string;
|
|
hasRings?: boolean;
|
|
ringRadius?: [number, number];
|
|
ringColor?: string;
|
|
hasMoon?: boolean;
|
|
initialAngle?: number;
|
|
onHover?: (name: string | null) => void;
|
|
}
|
|
|
|
function Planet({
|
|
name,
|
|
radius,
|
|
size,
|
|
speed,
|
|
color,
|
|
emissive = "#000000",
|
|
hasRings = false,
|
|
ringRadius = [1.2, 1.8],
|
|
ringColor = "#c084fc",
|
|
hasMoon = false,
|
|
initialAngle = 0,
|
|
onHover,
|
|
}: PlanetProps) {
|
|
const planetGroupRef = useRef<THREE.Group>(null!);
|
|
const planetMeshRef = useRef<THREE.Mesh>(null!);
|
|
const moonGroupRef = useRef<THREE.Group>(null!);
|
|
const angleRef = useRef(initialAngle);
|
|
|
|
useFrame((_, delta) => {
|
|
angleRef.current += delta * speed * 0.35;
|
|
const x = Math.cos(angleRef.current) * radius;
|
|
const z = Math.sin(angleRef.current) * radius;
|
|
|
|
if (planetGroupRef.current) {
|
|
planetGroupRef.current.position.set(x, 0, z);
|
|
}
|
|
if (planetMeshRef.current) {
|
|
planetMeshRef.current.rotation.y += delta * 1.2;
|
|
}
|
|
if (moonGroupRef.current) {
|
|
moonGroupRef.current.rotation.y += delta * 2.5;
|
|
}
|
|
});
|
|
|
|
return (
|
|
<group ref={planetGroupRef}>
|
|
{/* The Planet Sphere */}
|
|
<mesh
|
|
ref={planetMeshRef}
|
|
onPointerOver={(e) => {
|
|
e.stopPropagation();
|
|
onHover?.(name);
|
|
}}
|
|
onPointerOut={() => onHover?.(null)}
|
|
>
|
|
<sphereGeometry args={[size, 24, 24]} />
|
|
<meshStandardMaterial
|
|
color={color}
|
|
emissive={emissive}
|
|
emissiveIntensity={0.6}
|
|
roughness={0.3}
|
|
metalness={0.7}
|
|
/>
|
|
</mesh>
|
|
|
|
{/* Planetary Rings */}
|
|
{hasRings && (
|
|
<mesh rotation={[Math.PI / 2.5, 0.2, 0]}>
|
|
<ringGeometry args={[size * ringRadius[0], size * ringRadius[1], 48]} />
|
|
<meshStandardMaterial
|
|
color={ringColor}
|
|
emissive={ringColor}
|
|
emissiveIntensity={0.5}
|
|
transparent
|
|
opacity={0.65}
|
|
side={THREE.DoubleSide}
|
|
/>
|
|
</mesh>
|
|
)}
|
|
|
|
{/* Orbiting Moon */}
|
|
{hasMoon && (
|
|
<group ref={moonGroupRef}>
|
|
<mesh position={[size * 1.9, 0.15, 0]}>
|
|
<sphereGeometry args={[size * 0.28, 16, 16]} />
|
|
<meshStandardMaterial
|
|
color="#e2e8f0"
|
|
emissive="#94a3b8"
|
|
emissiveIntensity={0.4}
|
|
roughness={0.5}
|
|
/>
|
|
</mesh>
|
|
</group>
|
|
)}
|
|
</group>
|
|
);
|
|
}
|
|
|
|
// 4. Asteroid Belt (subtle, clean, not overcrowded)
|
|
function AsteroidBelt({ count = 180, innerR = 3.4, outerR = 4.2 }: { count?: number; innerR?: number; outerR?: number }) {
|
|
const beltRef = useRef<THREE.Points>(null!);
|
|
|
|
const [positions, colors] = useMemo(() => {
|
|
const pos = new Float32Array(count * 3);
|
|
const col = new Float32Array(count * 3);
|
|
const colorA = new THREE.Color("#94a3b8");
|
|
const colorB = new THREE.Color("#38bdf8");
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const r = innerR + Math.random() * (outerR - innerR);
|
|
const theta = Math.random() * Math.PI * 2;
|
|
const y = (Math.random() - 0.5) * 0.3;
|
|
|
|
pos[i * 3] = Math.cos(theta) * r;
|
|
pos[i * 3 + 1] = y;
|
|
pos[i * 3 + 2] = Math.sin(theta) * r;
|
|
|
|
const c = Math.random() > 0.4 ? colorA : colorB;
|
|
col[i * 3] = c.r;
|
|
col[i * 3 + 1] = c.g;
|
|
col[i * 3 + 2] = c.b;
|
|
}
|
|
|
|
return [pos, col];
|
|
}, [count, innerR, outerR]);
|
|
|
|
useFrame((_, delta) => {
|
|
if (beltRef.current) {
|
|
beltRef.current.rotation.y += delta * 0.06;
|
|
}
|
|
});
|
|
|
|
return (
|
|
<points ref={beltRef}>
|
|
<bufferGeometry>
|
|
<bufferAttribute attach="attributes-position" args={[positions, 3]} />
|
|
<bufferAttribute attach="attributes-color" args={[colors, 3]} />
|
|
</bufferGeometry>
|
|
<pointsMaterial
|
|
size={0.045}
|
|
vertexColors
|
|
transparent
|
|
opacity={0.55}
|
|
sizeAttenuation
|
|
/>
|
|
</points>
|
|
);
|
|
}
|
|
|
|
// 5. Composite Solar System Scene with smooth interactive tilt
|
|
function SolarSystemScene({ onHoverPlanet }: { onHoverPlanet: (name: string | null) => void }) {
|
|
const systemRef = useRef<THREE.Group>(null!);
|
|
|
|
useFrame((state) => {
|
|
if (!systemRef.current) return;
|
|
// Subtle interactive tilt following pointer
|
|
const targetRotX = 0.55 + (state.pointer.y * Math.PI) / 24;
|
|
const targetRotY = -0.25 + (state.pointer.x * Math.PI) / 20;
|
|
systemRef.current.rotation.x += (targetRotX - systemRef.current.rotation.x) * 0.04;
|
|
systemRef.current.rotation.y += (targetRotY - systemRef.current.rotation.y) * 0.04;
|
|
});
|
|
|
|
return (
|
|
<group ref={systemRef} rotation={[0.55, -0.25, 0]}>
|
|
{/* Central Star */}
|
|
<CentralStar />
|
|
|
|
{/* Planetary Tracks — calibrated so outermost radius is 6.4 */}
|
|
<OrbitTrack radius={1.7} color="#38bdf8" />
|
|
<OrbitTrack radius={2.7} color="#818cf8" />
|
|
<OrbitTrack radius={5.0} color="#c084fc" />
|
|
<OrbitTrack radius={6.4} color="#34d399" />
|
|
|
|
{/* Asteroid Belt between Orbit II and Orbit III */}
|
|
<AsteroidBelt count={190} innerR={3.5} outerR={4.3} />
|
|
|
|
{/* Planet 1: Chronos (Molten amber rocky core) */}
|
|
<Planet
|
|
name="CHRONOS // ORBIT I (TIME ENGINE)"
|
|
radius={1.7}
|
|
size={0.16}
|
|
speed={1.5}
|
|
color="#fb923c"
|
|
emissive="#ea580c"
|
|
initialAngle={0.5}
|
|
onHover={onHoverPlanet}
|
|
/>
|
|
|
|
{/* Planet 2: Aetheria (Teal Gem with Moon) */}
|
|
<Planet
|
|
name="AETHERIA // ORBIT II (FLAGSHIP SECTOR)"
|
|
radius={2.7}
|
|
size={0.25}
|
|
speed={1.0}
|
|
color="#22d3ee"
|
|
emissive="#0891b2"
|
|
hasMoon={true}
|
|
initialAngle={2.2}
|
|
onHover={onHoverPlanet}
|
|
/>
|
|
|
|
{/* Planet 3: Nebula-IX (Ringed Gas Giant) */}
|
|
<Planet
|
|
name="NEBULA-IX // ORBIT III (CREATIVE GAS GIANT)"
|
|
radius={5.0}
|
|
size={0.42}
|
|
speed={0.55}
|
|
color="#a855f7"
|
|
emissive="#7e22ce"
|
|
hasRings={true}
|
|
ringRadius={[1.3, 1.9]}
|
|
ringColor="#c084fc"
|
|
hasMoon={true}
|
|
initialAngle={4.1}
|
|
onHover={onHoverPlanet}
|
|
/>
|
|
|
|
{/* Planet 4: Void-Zero (Outer Starlight World) */}
|
|
<Planet
|
|
name="VOID-ZERO // ORBIT IV (DEEP EXPERIMENTAL)"
|
|
radius={6.4}
|
|
size={0.3}
|
|
speed={0.35}
|
|
color="#34d399"
|
|
emissive="#059669"
|
|
hasMoon={true}
|
|
initialAngle={5.6}
|
|
onHover={onHoverPlanet}
|
|
/>
|
|
</group>
|
|
);
|
|
}
|
|
|
|
export default function HeroSolarSystem() {
|
|
const [hoveredPlanet, setHoveredPlanet] = useState<string | null>(null);
|
|
|
|
return (
|
|
<div className="relative w-full aspect-square max-w-[540px] max-h-[540px] sm:max-h-[580px] flex items-center justify-center select-none mx-auto overflow-visible">
|
|
{/* Background soft ambient radial lighting */}
|
|
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
|
<div className="w-64 h-64 sm:w-80 sm:h-80 bg-cyan-500/10 rounded-full blur-[80px]" />
|
|
<div className="w-56 h-56 sm:w-72 sm:h-72 bg-purple-600/10 rounded-full blur-[80px]" />
|
|
</div>
|
|
|
|
{/*
|
|
Camera position: [0, 11, 16] with fov: 42
|
|
At distance ~19.4, visible height is 15.6 units and width is >18 units.
|
|
Outermost orbit radius is 6.4 (diameter 12.8).
|
|
-> 100% Guaranteed ZERO CROPPING at all angles.
|
|
*/}
|
|
<Canvas
|
|
camera={{ position: [0, 11, 16], fov: 42 }}
|
|
dpr={[1, 1.5]}
|
|
gl={{ antialias: true, alpha: true }}
|
|
className="w-full h-full cursor-grab active:cursor-grabbing overflow-visible"
|
|
>
|
|
<ambientLight intensity={0.4} />
|
|
<directionalLight position={[10, 15, 10]} intensity={1.2} color="#ffffff" />
|
|
<directionalLight position={[-10, -10, -5]} intensity={0.6} color="#818cf8" />
|
|
<SolarSystemScene onHoverPlanet={setHoveredPlanet} />
|
|
</Canvas>
|
|
|
|
{/* Interactive HUD Telemetry Card */}
|
|
<div className="absolute bottom-2 left-1/2 -translate-x-1/2 sm:left-2 sm:translate-x-0 glass-panel px-3.5 py-1.5 rounded-xl border border-cyan-500/30 flex items-center gap-2.5 text-[11px] font-mono tracking-wider shadow-[0_0_20px_rgba(56,189,248,0.2)] pointer-events-none">
|
|
<div className="flex items-center gap-1.5">
|
|
<span className="w-2 h-2 rounded-full bg-cyan-400 animate-ping" />
|
|
<span className="text-cyan-300 font-bold">AGHATIS ORBITAL</span>
|
|
</div>
|
|
<span className="text-slate-500">|</span>
|
|
<span className="text-slate-300">
|
|
{hoveredPlanet ? (
|
|
<span className="text-cyan-400 font-bold">{hoveredPlanet}</span>
|
|
) : (
|
|
"4 ACTIVE ORBITS • TILT TO EXPLORE"
|
|
)}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Orbit Speed & System Badge */}
|
|
<div className="absolute top-2 right-2 hidden sm:flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-indigo-950/40 border border-indigo-500/30 text-[10px] font-mono text-indigo-300">
|
|
<span className="w-1.5 h-1.5 rounded-full bg-indigo-400" />
|
|
<span>SIMULATION: 1.0X</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|