polyguard-openenv-workbench / polyguard-rl /app /ui /frontend /src /components /MetaverseBackdrop.tsx
| import { useEffect, useRef } from "react"; | |
| type Star = { | |
| x: number; | |
| y: number; | |
| z: number; | |
| size: number; | |
| speed: number; | |
| }; | |
| function createStars(count: number): Star[] { | |
| return Array.from({ length: count }, () => ({ | |
| x: Math.random() * 2 - 1, | |
| y: Math.random() * 2 - 1, | |
| z: Math.random(), | |
| size: Math.random() * 1.4 + 0.25, | |
| speed: Math.random() * 0.00055 + 0.00018, | |
| })); | |
| } | |
| function StarCanvas() { | |
| const canvasRef = useRef<HTMLCanvasElement | null>(null); | |
| useEffect(() => { | |
| const canvas = canvasRef.current; | |
| const context = canvas?.getContext("2d"); | |
| if (!canvas || !context) return undefined; | |
| let animationFrame = 0; | |
| let width = 0; | |
| let height = 0; | |
| let centerX = 0; | |
| let centerY = 0; | |
| const stars = createStars(680); | |
| const resize = () => { | |
| const pixelRatio = Math.min(window.devicePixelRatio || 1, 2); | |
| width = window.innerWidth; | |
| height = window.innerHeight; | |
| centerX = width / 2; | |
| centerY = height / 2; | |
| canvas.width = Math.floor(width * pixelRatio); | |
| canvas.height = Math.floor(height * pixelRatio); | |
| canvas.style.width = `${width}px`; | |
| canvas.style.height = `${height}px`; | |
| context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); | |
| }; | |
| const draw = () => { | |
| context.clearRect(0, 0, width, height); | |
| context.globalCompositeOperation = "lighter"; | |
| stars.forEach((star) => { | |
| star.z -= star.speed; | |
| if (star.z <= 0.02) { | |
| star.x = Math.random() * 2 - 1; | |
| star.y = Math.random() * 2 - 1; | |
| star.z = 1; | |
| } | |
| const perspective = 1 / star.z; | |
| const x = centerX + star.x * perspective * centerX; | |
| const y = centerY + star.y * perspective * centerY; | |
| const opacity = Math.max(0, Math.min(1, 1.15 - star.z)); | |
| const radius = star.size * perspective * 0.85; | |
| context.beginPath(); | |
| context.fillStyle = `rgba(210, 246, 255, ${opacity})`; | |
| context.arc(x, y, radius, 0, Math.PI * 2); | |
| context.fill(); | |
| }); | |
| animationFrame = window.requestAnimationFrame(draw); | |
| }; | |
| resize(); | |
| draw(); | |
| window.addEventListener("resize", resize); | |
| return () => { | |
| window.removeEventListener("resize", resize); | |
| window.cancelAnimationFrame(animationFrame); | |
| }; | |
| }, []); | |
| return <canvas ref={canvasRef} />; | |
| } | |
| export default function MetaverseBackdrop() { | |
| return ( | |
| <div className="metaverse-backdrop" aria-hidden="true"> | |
| <video className="blackhole-video" autoPlay muted loop playsInline preload="auto"> | |
| <source src="/blackhole.webm" type="video/webm" /> | |
| </video> | |
| <div className="stars-canvas"> | |
| <StarCanvas /> | |
| </div> | |
| <div className="nebula-orb orb-one" /> | |
| <div className="nebula-orb orb-two" /> | |
| <div className="nebula-grid" /> | |
| <div className="cosmic-vignette" /> | |
| </div> | |
| ); | |
| } | |