// Animated starfield — canvas, drawn over the deep navy. // Stars twinkle (alpha shimmer), with a few "shooting" highlights that drift slowly. // Density controlled by props.intensity (0..2). const { useEffect, useRef } = React; function Starfield({ intensity = 1, density = 0.00012 }) { const ref = useRef(null); useEffect(() => { const canvas = ref.current; if (!canvas) return; const ctx = canvas.getContext("2d"); let raf, w, h, dpr; let stars = []; let shooters = []; const goldA = (a) => `rgba(231, 212, 155, ${a})`; const goldB = (a) => `rgba(244, 232, 200, ${a})`; const goldC = (a) => `rgba(212, 180, 106, ${a})`; function resize() { dpr = Math.min(window.devicePixelRatio || 1, 2); const r = canvas.getBoundingClientRect(); w = r.width; h = r.height; canvas.width = Math.floor(w * dpr); canvas.height = Math.floor(h * dpr); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); build(); } function build() { const count = Math.floor(w * h * density * intensity); stars = new Array(count).fill(0).map(() => { const r = Math.pow(Math.random(), 2.4); return { x: Math.random() * w, y: Math.random() * h * 0.95, // arc bias: more stars near top arc to echo the logo size: 0.4 + r * 2.6, base: 0.15 + Math.random() * 0.7, phase: Math.random() * Math.PI * 2, speed: 0.4 + Math.random() * 1.6, // big sparkle ones get a cross big: Math.random() < 0.05, // hue choice hue: Math.random() < 0.7 ? "warm" : "white", }; }); } function spawnShooter() { // very rare, very slow drift across upper third shooters.push({ x: -40, y: 40 + Math.random() * (h * 0.4), vx: 0.6 + Math.random() * 0.8, vy: 0.05 + Math.random() * 0.1, life: 0, maxLife: 320 + Math.random() * 200, }); } let tick = 0; function frame() { tick++; ctx.clearRect(0, 0, w, h); // Subtle nebular wash near the top const grad = ctx.createRadialGradient(w * 0.5, -h * 0.1, 0, w * 0.5, h * 0.4, h * 0.85); grad.addColorStop(0, "rgba(40, 30, 70, 0.55)"); grad.addColorStop(0.45, "rgba(20, 18, 48, 0.25)"); grad.addColorStop(1, "rgba(7, 9, 26, 0)"); ctx.fillStyle = grad; ctx.fillRect(0, 0, w, h); const t = tick / 60; for (let i = 0; i < stars.length; i++) { const s = stars[i]; const a = s.base * (0.55 + 0.45 * Math.sin(t * s.speed + s.phase)); ctx.beginPath(); ctx.fillStyle = s.hue === "warm" ? goldA(a) : goldB(a); ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2); ctx.fill(); if (s.big) { ctx.strokeStyle = goldC(a * 0.55); ctx.lineWidth = 0.6; ctx.beginPath(); const L = s.size * 4; ctx.moveTo(s.x - L, s.y); ctx.lineTo(s.x + L, s.y); ctx.moveTo(s.x, s.y - L); ctx.lineTo(s.x, s.y + L); ctx.stroke(); } } // shooters if (Math.random() < 0.0025 * intensity && shooters.length < 2) spawnShooter(); for (let i = shooters.length - 1; i >= 0; i--) { const sh = shooters[i]; sh.x += sh.vx; sh.y += sh.vy; sh.life++; const lifeT = sh.life / sh.maxLife; const a = Math.sin(Math.PI * lifeT) * 0.7; const tailX = sh.x - sh.vx * 60; const tailY = sh.y - sh.vy * 60; const lg = ctx.createLinearGradient(tailX, tailY, sh.x, sh.y); lg.addColorStop(0, "rgba(244, 232, 200, 0)"); lg.addColorStop(1, `rgba(244, 232, 200, ${a})`); ctx.strokeStyle = lg; ctx.lineWidth = 1.1; ctx.beginPath(); ctx.moveTo(tailX, tailY); ctx.lineTo(sh.x, sh.y); ctx.stroke(); ctx.fillStyle = `rgba(244, 232, 200, ${a})`; ctx.beginPath(); ctx.arc(sh.x, sh.y, 1.4, 0, Math.PI * 2); ctx.fill(); if (sh.life > sh.maxLife || sh.x > w + 60) shooters.splice(i, 1); } raf = requestAnimationFrame(frame); } resize(); frame(); window.addEventListener("resize", resize); return () => { cancelAnimationFrame(raf); window.removeEventListener("resize", resize); }; }, [intensity, density]); return ; } window.Starfield = Starfield;