Spaces:
Running
Running
| import { useEffect, useRef } from "react"; | |
| // Identical ellipses, evenly spaced tilts (45° apart) | |
| const ORBITALS = [ | |
| { icon: "📷", label: "Image", tilt: 0, rx: 130, ry: 45, duration: 10, phase: 0 }, | |
| { icon: "🎤", label: "Audio", tilt: 45, rx: 130, ry: 45, duration: 13, phase: 0 }, | |
| { icon: "💬", label: "Text", tilt: 90, rx: 130, ry: 45, duration: 11, phase: 0 }, | |
| { icon: "🎬", label: "Video", tilt: 135, rx: 130, ry: 45, duration: 15, phase: 0 }, | |
| ]; | |
| // Precompute tilt radians | |
| const ORBITAL_DATA = ORBITALS.map((o) => ({ | |
| ...o, | |
| tiltRad: (o.tilt * Math.PI) / 180, | |
| })); | |
| export default function OrbitalHero() { | |
| const itemRefs = useRef([]); | |
| useEffect(() => { | |
| let frame; | |
| const start = performance.now(); | |
| function tick() { | |
| const time = (performance.now() - start) / 1000; | |
| for (let i = 0; i < ORBITAL_DATA.length; i++) { | |
| const o = ORBITAL_DATA[i]; | |
| const el = itemRefs.current[i]; | |
| if (!el) continue; | |
| const angle = ((time / o.duration) * Math.PI * 2) + o.phase; | |
| const ex = o.rx * Math.cos(angle); | |
| const ey = o.ry * Math.sin(angle); | |
| const x = ex * Math.cos(o.tiltRad) - ey * Math.sin(o.tiltRad); | |
| const y = ex * Math.sin(o.tiltRad) + ey * Math.cos(o.tiltRad); | |
| const z = Math.sin(angle); | |
| el.style.left = `calc(50% + ${x}px)`; | |
| el.style.top = `calc(50% + ${y}px)`; | |
| el.style.zIndex = z > 0 ? 20 : 1; | |
| el.style.opacity = 0.5 + z * 0.5; | |
| } | |
| frame = requestAnimationFrame(tick); | |
| } | |
| frame = requestAnimationFrame(tick); | |
| return () => cancelAnimationFrame(frame); | |
| }, []); | |
| return ( | |
| <div className="relative w-80 h-80 mx-auto"> | |
| <svg className="absolute inset-0 w-full h-full" viewBox="-160 -160 320 320"> | |
| {ORBITAL_DATA.map((o) => ( | |
| <ellipse | |
| key={o.label} | |
| cx="0" | |
| cy="0" | |
| rx={o.rx} | |
| ry={o.ry} | |
| fill="none" | |
| stroke="var(--color-text-secondary)" | |
| opacity="0.15" | |
| strokeWidth="1" | |
| transform={`rotate(${o.tilt})`} | |
| /> | |
| ))} | |
| </svg> | |
| <div className="absolute inset-0 flex items-center justify-center z-10"> | |
| <div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-[#3186FF] to-[#4FA0FF] flex items-center justify-center text-white text-2xl font-bold shadow-[0_0_40px_rgba(60,144,255,0.3)]"> | |
| G | |
| </div> | |
| </div> | |
| <div className="absolute inset-0 flex items-center justify-center pointer-events-none"> | |
| <div className="w-28 h-28 rounded-full bg-[var(--color-blue)]/8 animate-pulse" /> | |
| </div> | |
| {ORBITAL_DATA.map((o, i) => ( | |
| <div | |
| key={o.label} | |
| ref={(el) => (itemRefs.current[i] = el)} | |
| className="absolute flex flex-col items-center gap-0.5 px-2.5 py-1.5 bg-[var(--color-surface)]/90 border border-[var(--color-outline)] rounded-xl backdrop-blur-sm" | |
| style={{ transform: "translate(-50%, -50%)" }} | |
| > | |
| <span className="text-base leading-none">{o.icon}</span> | |
| <span className="text-[9px] text-[var(--color-text-secondary)] font-medium leading-none">{o.label}</span> | |
| </div> | |
| ))} | |
| </div> | |
| ); | |
| } | |