// Nebula Tracker — animated starfield, parallax & shooting stars. (function () { const canvas = document.getElementById('starfield'); if (!canvas) return; const ctx = canvas.getContext('2d'); let w, h, stars = [], shootingStars = []; let mouseX = 0, mouseY = 0; const STAR_COLORS = ['#ffffff', '#bfe9ff', '#d7c4ff', '#9fffe0']; function resize() { w = canvas.width = window.innerWidth; h = canvas.height = window.innerHeight; buildStars(); } function buildStars() { const count = Math.floor((w * h) / 6000); stars = []; for (let i = 0; i < count; i++) { const depth = Math.random(); stars.push({ x: Math.random() * w, y: Math.random() * h, r: depth * 1.6 + 0.3, depth: depth, twinkle: Math.random() * Math.PI * 2, speed: 0.002 + Math.random() * 0.006, color: STAR_COLORS[(Math.random() * STAR_COLORS.length) | 0] }); } } function spawnShootingStar() { const startX = Math.random() * w; const startY = Math.random() * h * 0.4; shootingStars.push({ x: startX, y: startY, len: 80 + Math.random() * 120, speed: 6 + Math.random() * 6, angle: Math.PI / 4, life: 1 }); } function draw() { ctx.clearRect(0, 0, w, h); // Parallax offset based on mouse position. const offX = (mouseX - w / 2) * 0.01; const offY = (mouseY - h / 2) * 0.01; for (const s of stars) { s.twinkle += s.speed * 20; const alpha = 0.4 + Math.abs(Math.sin(s.twinkle)) * 0.6; const px = s.x + offX * s.depth * 6; const py = s.y + offY * s.depth * 6; ctx.beginPath(); ctx.globalAlpha = alpha; ctx.fillStyle = s.color; ctx.shadowBlur = s.r * 4; ctx.shadowColor = s.color; ctx.arc(px, py, s.r, 0, Math.PI * 2); ctx.fill(); } ctx.globalAlpha = 1; ctx.shadowBlur = 0; // Shooting stars. for (let i = shootingStars.length - 1; i >= 0; i--) { const ss = shootingStars[i]; ss.x += Math.cos(ss.angle) * ss.speed; ss.y += Math.sin(ss.angle) * ss.speed; ss.life -= 0.012; const tailX = ss.x - Math.cos(ss.angle) * ss.len; const tailY = ss.y - Math.sin(ss.angle) * ss.len; const grad = ctx.createLinearGradient(ss.x, ss.y, tailX, tailY); grad.addColorStop(0, `rgba(255,255,255,${ss.life})`); grad.addColorStop(1, 'rgba(122,92,255,0)'); ctx.strokeStyle = grad; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(ss.x, ss.y); ctx.lineTo(tailX, tailY); ctx.stroke(); if (ss.life <= 0 || ss.x > w + 200 || ss.y > h + 200) { shootingStars.splice(i, 1); } } requestAnimationFrame(draw); } window.addEventListener('resize', resize); window.addEventListener('mousemove', function (e) { mouseX = e.clientX; mouseY = e.clientY; }); // Occasionally launch a shooting star. setInterval(function () { if (Math.random() < 0.6) spawnShootingStar(); }, 2600); resize(); draw(); })(); // Add a subtle launch ripple to buttons. document.addEventListener('click', function (e) { const btn = e.target.closest('.btn-launch'); if (!btn) return; btn.animate( [ { boxShadow: '0 0 18px rgba(122,92,255,0.55)' }, { boxShadow: '0 0 40px rgba(32,227,255,0.95)' }, { boxShadow: '0 0 18px rgba(122,92,255,0.55)' } ], { duration: 500, easing: 'ease-out' } ); });