/** * Space Background — Glass Shards · Animated Nebula · Drifting Stars · Shooting Stars * No hex grid. Cursor scatters floating glass fragments. */ (function () { 'use strict'; let canvas, ctx; let W = 0, H = 0; let mouse = { x: -2000, y: -2000 }; let rafId; let initialized = false; let time = 0; /* ────────────────────────────────────────────── NEBULA BLOBS (animated radial gradients) — dimmed ────────────────────────────────────────────── */ const NEBULAS = [ { px: 0.12, py: 0.25, pr: 0.50, cr: 255, cg: 80, cb: 0, a: 0.040, spx: 0.00014, spy: 0.00007, phase: 0.0 }, { px: 0.82, py: 0.55, pr: 0.55, cr: 0, cg: 255, cb: 136, a: 0.028, spx:-0.00009, spy: 0.00011, phase: 2.1 }, { px: 0.50, py: 0.88, pr: 0.42, cr: 0, cg: 180, cb: 255, a: 0.022, spx: 0.00007, spy:-0.00009, phase: 4.3 }, { px: 0.72, py: 0.12, pr: 0.38, cr: 160, cg: 0, cb: 255, a: 0.016, spx:-0.00010, spy: 0.00005, phase: 1.5 }, { px: 0.35, py: 0.65, pr: 0.32, cr: 255, cg: 140, cb: 0, a: 0.016, spx: 0.00005, spy:-0.00006, phase: 3.7 }, ]; function drawNebulas() { for (let i = 0; i < NEBULAS.length; i++) { const n = NEBULAS[i]; const ox = Math.sin(time * n.spx * 800 + n.phase) * W * 0.10; const oy = Math.cos(time * n.spy * 800 + n.phase + 1.2) * H * 0.10; const cx = n.px * W + ox; const cy = n.py * H + oy; const rad = n.pr * Math.min(W, H); const pa = n.a * (0.65 + 0.35 * Math.sin(time * 0.28 + n.phase)); const g = ctx.createRadialGradient(cx, cy, 0, cx, cy, rad); g.addColorStop(0, `rgba(${n.cr},${n.cg},${n.cb},${pa})`); g.addColorStop(0.45,`rgba(${n.cr},${n.cg},${n.cb},${pa * 0.35})`); g.addColorStop(1, `rgba(${n.cr},${n.cg},${n.cb},0)`); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(cx, cy, rad, 0, Math.PI * 2); ctx.fill(); } } /* ────────────────────────────────────────────── DRIFTING STARS — dimmed halos ────────────────────────────────────────────── */ const STAR_COUNT = 220; let stars = []; function initStars() { stars = []; for (let i = 0; i < STAR_COUNT; i++) { stars.push({ x: Math.random() * W, y: Math.random() * H, r: 0.35 + Math.random() * 1.65, vx: (Math.random() - 0.5) * 0.055, vy: 0.018 + Math.random() * 0.055, phase: Math.random() * Math.PI * 2, twinkleSpeed: 0.4 + Math.random() * 1.6, // 0=white 1=neon-green 2=neon-teal type: Math.random() < 0.78 ? 0 : (Math.random() < 0.5 ? 1 : 2), }); } } function drawStars() { for (let i = 0; i < stars.length; i++) { const s = stars[i]; const tw = 0.25 + 0.75 * (0.5 + 0.5 * Math.sin(time * s.twinkleSpeed + s.phase)); s.x += s.vx; s.y += s.vy; if (s.y > H + 2) { s.y = -2; s.x = Math.random() * W; } if (s.x < -2) s.x = W + 2; if (s.x > W + 2) s.x = -2; const clr = s.type === 0 ? `rgba(255,255,255,${tw * 0.70})` : s.type === 1 ? `rgba(0,255,136,${tw * 0.50})` : `rgba(0,220,255,${tw * 0.50})`; ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2); ctx.fillStyle = clr; ctx.fill(); // Halo only on larger stars, dimmed if (s.r > 1.1) { const halo = ctx.createRadialGradient(s.x, s.y, 0, s.x, s.y, s.r * 3.5); const ha = tw * (s.type === 0 ? 0.14 : 0.10); halo.addColorStop(0, s.type === 0 ? `rgba(255,255,255,${ha})` : s.type === 1 ? `rgba(0,255,136,${ha})` : `rgba(0,220,255,${ha})`); halo.addColorStop(1, 'rgba(0,0,0,0)'); ctx.beginPath(); ctx.arc(s.x, s.y, s.r * 3.5, 0, Math.PI * 2); ctx.fillStyle = halo; ctx.fill(); } } } /* ────────────────────────────────────────────── SHOOTING STARS ────────────────────────────────────────────── */ const MAX_SHOOTING_STARS = 3; let shootingStars = []; let nextShootingStarAt = 0; // time value when next star spawns function spawnShootingStar() { // Spawn from top or right edge, travel down-left or down-right const fromRight = Math.random() < 0.5; const startX = fromRight ? W * (0.5 + Math.random() * 0.6) : W * Math.random() * 0.7; const startY = Math.random() * H * 0.45; const angle = (Math.PI / 4) + (Math.random() - 0.5) * 0.6; // ~45° downward const speed = 6 + Math.random() * 9; const length = 80 + Math.random() * 160; shootingStars.push({ x: startX, y: startY, vx: Math.cos(angle) * speed * (fromRight ? -1 : 1), vy: Math.sin(angle) * speed, length, alpha: 0, fadeIn: true, life: 0, maxLife: (length / speed) * 1.6, // frames to live }); } function updateDrawShootingStars() { // Possibly spawn a new one if (shootingStars.length < MAX_SHOOTING_STARS && time > nextShootingStarAt) { spawnShootingStar(); // Next star between 4–14 seconds of time units (time += 0.016/frame) nextShootingStarAt = time + 4 + Math.random() * 10; } for (let i = shootingStars.length - 1; i >= 0; i--) { const s = shootingStars[i]; s.life++; // Fade in quickly, fade out near end if (s.life < 8) { s.alpha = s.life / 8; } else if (s.life > s.maxLife - 10) { s.alpha = Math.max(0, (s.maxLife - s.life) / 10); } else { s.alpha = 1; } s.x += s.vx; s.y += s.vy; // Draw trail const tailX = s.x - (s.vx / Math.hypot(s.vx, s.vy)) * s.length; const tailY = s.y - (s.vy / Math.hypot(s.vx, s.vy)) * s.length; const grad = ctx.createLinearGradient(s.x, s.y, tailX, tailY); grad.addColorStop(0, `rgba(255,255,255,${s.alpha * 0.90})`); grad.addColorStop(0.15,`rgba(220,235,255,${s.alpha * 0.55})`); grad.addColorStop(1, `rgba(180,210,255,0)`); ctx.beginPath(); ctx.moveTo(s.x, s.y); ctx.lineTo(tailX, tailY); ctx.strokeStyle = grad; ctx.lineWidth = 1.5; ctx.stroke(); // Bright head dot ctx.beginPath(); ctx.arc(s.x, s.y, 1.4, 0, Math.PI * 2); ctx.fillStyle = `rgba(255,255,255,${s.alpha * 0.95})`; ctx.fill(); // Remove if expired or off-screen if (s.life >= s.maxLife || s.x < -50 || s.x > W + 50 || s.y > H + 50) { shootingStars.splice(i, 1); } } } /* ────────────────────────────────────────────── GLASS SHARDS — fixed physics ────────────────────────────────────────────── */ const SHARD_COUNT = 30; const SHARD_RADIUS = 230; // mouse influence radius const SCATTER_FORCE = 3.0; const ROT_V_MAX = 0.018; // cap on rotation speed const SHARD_COLORS = [ { r: 255, g: 102, b: 0 }, // neon orange { r: 0, g: 255, b: 136 }, // neon green { r: 0, g: 220, b: 255 }, // neon teal { r: 255, g: 255, b: 255 }, // white { r: 255, g: 180, b: 0 }, // amber ]; let shards = []; function buildVerts(size, sides) { const verts = []; const base = Math.random() * Math.PI * 2; for (let i = 0; i < sides; i++) { const a = base + (Math.PI * 2 * i / sides) + (Math.random() - 0.5) * 0.65; const r = size * (0.55 + Math.random() * 0.45); verts.push([Math.cos(a) * r, Math.sin(a) * r]); } return verts; } function initShards() { shards = []; for (let i = 0; i < SHARD_COUNT; i++) { const size = 11 + Math.random() * 42; const sides = 3 + Math.floor(Math.random() * 3); // 3–5 sides const col = SHARD_COLORS[Math.floor(Math.random() * SHARD_COLORS.length)]; shards.push({ x: Math.random() * W, y: Math.random() * H, vx: (Math.random() - 0.5) * 0.22, vy: (Math.random() - 0.5) * 0.22, rot: Math.random() * Math.PI * 2, rotV: (Math.random() - 0.5) * 0.003, // very gentle initial spin verts: buildVerts(size, sides), size, col, alpha: 0.18 + Math.random() * 0.24, alphaTarget: 0.18 + Math.random() * 0.24, svx: 0, svy: 0, phase: Math.random() * Math.PI * 2, floatSpeed: 0.20 + Math.random() * 0.45, // slower float }); } } function drawShard(s) { ctx.save(); ctx.translate(s.x, s.y); ctx.rotate(s.rot); const { r, g, b } = s.col; const al = s.alpha; ctx.beginPath(); ctx.moveTo(s.verts[0][0], s.verts[0][1]); for (let i = 1; i < s.verts.length; i++) ctx.lineTo(s.verts[i][0], s.verts[i][1]); ctx.closePath(); // Glass fill const fill = ctx.createLinearGradient(-s.size, -s.size, s.size * 0.6, s.size * 0.6); fill.addColorStop(0, `rgba(${r},${g},${b},${al * 0.18})`); fill.addColorStop(0.45,`rgba(255,255,255,${al * 0.08})`); fill.addColorStop(1, `rgba(${r},${g},${b},${al * 0.03})`); ctx.fillStyle = fill; ctx.fill(); // Neon outline — dimmed ctx.shadowColor = `rgba(${r},${g},${b},0.45)`; ctx.shadowBlur = 7; ctx.strokeStyle = `rgba(${r},${g},${b},${al * 0.70})`; ctx.lineWidth = 1.0; ctx.stroke(); // Soft outer glow — dimmed ctx.shadowBlur = 14; ctx.strokeStyle = `rgba(${r},${g},${b},${al * 0.18})`; ctx.lineWidth = 2.0; ctx.stroke(); ctx.shadowBlur = 0; // Inner highlight if (s.verts.length >= 2) { ctx.beginPath(); ctx.moveTo(s.verts[0][0] * 0.55, s.verts[0][1] * 0.55); ctx.lineTo(s.verts[1][0] * 0.55, s.verts[1][1] * 0.55); ctx.strokeStyle = `rgba(255,255,255,${al * 0.40})`; ctx.lineWidth = 0.8; ctx.shadowBlur = 3; ctx.shadowColor = 'rgba(255,255,255,0.3)'; ctx.stroke(); ctx.shadowBlur = 0; } ctx.restore(); } function updateShards() { const infSq = SHARD_RADIUS * SHARD_RADIUS; for (let i = 0; i < shards.length; i++) { const s = shards[i]; // Gentle float bob s.vy += Math.sin(time * s.floatSpeed + s.phase) * 0.002; // Mouse scatter if (mouse.x > -1000) { const dx = s.x - mouse.x; const dy = s.y - mouse.y; const distSq = dx * dx + dy * dy; if (distSq < infSq) { const dist = Math.sqrt(distSq); const force = (1 - dist / SHARD_RADIUS) * SCATTER_FORCE; const ang = Math.atan2(dy, dx); s.svx += Math.cos(ang) * force * 0.065; s.svy += Math.sin(ang) * force * 0.065; // Small nudge to rotation — NOT multiplicative s.rotV += (Math.random() - 0.5) * 0.004 * (1 - dist / SHARD_RADIUS); // Brighten on interaction s.alphaTarget = Math.min(0.75, s.alphaTarget + 0.03); } } // Rotation damping — keeps shards from spinning endlessly s.rotV *= 0.96; // Hard cap on rotation speed if (s.rotV > ROT_V_MAX) s.rotV = ROT_V_MAX; if (s.rotV < -ROT_V_MAX) s.rotV = -ROT_V_MAX; // Scatter velocity damping s.svx *= 0.92; s.svy *= 0.92; s.vx *= 0.998; s.vy *= 0.998; // Apply s.x += s.vx + s.svx; s.y += s.vy + s.svy; s.rot += s.rotV; // Fade alpha toward target s.alpha += (s.alphaTarget - s.alpha) * 0.025; // Slowly restore target const base = 0.18; if (s.alphaTarget > base) s.alphaTarget -= 0.006; // Edge wrap const m = s.size + 10; if (s.x < -m) s.x = W + m; if (s.x > W + m) s.x = -m; if (s.y < -m) s.y = H + m; if (s.y > H + m) s.y = -m; } } /* ────────────────────────────────────────────── CURSOR AURA — dimmed ────────────────────────────────────────────── */ function drawCursorAura() { const r = 130 + Math.sin(time * 1.8) * 22; const aura = ctx.createRadialGradient(mouse.x, mouse.y, 0, mouse.x, mouse.y, r); aura.addColorStop(0, 'rgba(255,102,0,0.04)'); aura.addColorStop(0.5, 'rgba(255,102,0,0.012)'); aura.addColorStop(1, 'rgba(255,102,0,0)'); ctx.fillStyle = aura; ctx.beginPath(); ctx.arc(mouse.x, mouse.y, r, 0, Math.PI * 2); ctx.fill(); } /* ────────────────────────────────────────────── MAIN LOOP ────────────────────────────────────────────── */ function animate() { if (!initialized) return; time += 0.016; ctx.clearRect(0, 0, W, H); drawNebulas(); drawStars(); updateDrawShootingStars(); updateShards(); for (let i = 0; i < shards.length; i++) drawShard(shards[i]); if (mouse.x > -1000) drawCursorAura(); rafId = requestAnimationFrame(animate); } /* ────────────────────────────────────────────── MOBILE — one-shot static render ────────────────────────────────────────────── */ function initMobileStatic() { const c = document.getElementById('hexCanvas'); if (!c) return; const dpr = Math.min(window.devicePixelRatio || 1, 2); const w = window.innerWidth; const h = window.innerHeight; c.width = w * dpr; c.height = h * dpr; c.style.width = w + 'px'; c.style.height = h + 'px'; const cx = c.getContext('2d'); cx.scale(dpr, dpr); // Nebula blobs — dimmed const g1 = cx.createRadialGradient(w*0.15, h*0.3, 0, w*0.15, h*0.3, w*0.5); g1.addColorStop(0, 'rgba(255,80,0,0.04)'); g1.addColorStop(1, 'rgba(255,80,0,0)'); cx.fillStyle = g1; cx.fillRect(0, 0, w, h); const g2 = cx.createRadialGradient(w*0.82, h*0.65, 0, w*0.82, h*0.65, w*0.45); g2.addColorStop(0, 'rgba(0,255,136,0.03)'); g2.addColorStop(1, 'rgba(0,255,136,0)'); cx.fillStyle = g2; cx.fillRect(0, 0, w, h); const g3 = cx.createRadialGradient(w*0.5, h*0.85, 0, w*0.5, h*0.85, w*0.4); g3.addColorStop(0, 'rgba(0,180,255,0.025)'); g3.addColorStop(1, 'rgba(0,180,255,0)'); cx.fillStyle = g3; cx.fillRect(0, 0, w, h); // Stars for (let i = 0; i < 130; i++) { const sx = Math.random() * w; const sy = Math.random() * h; const sr = 0.35 + Math.random() * 1.5; const sa = 0.20 + Math.random() * 0.55; cx.beginPath(); cx.arc(sx, sy, sr, 0, Math.PI * 2); cx.fillStyle = `rgba(255,255,255,${sa})`; cx.fill(); } // Glass shards const MCOLS = [[255,102,0],[0,255,136],[0,220,255],[255,255,255],[255,180,0]]; for (let i = 0; i < 14; i++) { const sx = Math.random() * w; const sy = Math.random() * h; const ss = 10 + Math.random() * 35; const sides = 3 + Math.floor(Math.random() * 3); const [cr,cg,cb] = MCOLS[Math.floor(Math.random() * MCOLS.length)]; cx.save(); cx.translate(sx, sy); cx.rotate(Math.random() * Math.PI * 2); cx.beginPath(); for (let j = 0; j < sides; j++) { const a = (Math.PI * 2 * j / sides) + (Math.random()-0.5)*0.6; const r = ss * (0.55 + Math.random() * 0.45); j === 0 ? cx.moveTo(Math.cos(a)*r, Math.sin(a)*r) : cx.lineTo(Math.cos(a)*r, Math.sin(a)*r); } cx.closePath(); cx.fillStyle = `rgba(${cr},${cg},${cb},0.04)`; cx.strokeStyle = `rgba(${cr},${cg},${cb},0.38)`; cx.lineWidth = 1; cx.fill(); cx.shadowColor = `rgba(${cr},${cg},${cb},0.45)`; cx.shadowBlur = 6; cx.stroke(); cx.shadowBlur = 0; cx.restore(); } } /* ────────────────────────────────────────────── INIT ────────────────────────────────────────────── */ function init() { if (window.innerWidth <= 768 || ('ontouchstart' in window)) { initMobileStatic(); return; } canvas = document.getElementById('hexCanvas'); if (!canvas) return; ctx = canvas.getContext('2d', { alpha: true }); resize(); window.addEventListener('resize', debounce(resize, 200)); document.addEventListener('mousemove', e => { mouse.x = e.clientX; mouse.y = e.clientY; }, { passive: true }); document.addEventListener('mouseleave', () => { mouse.x = -2000; mouse.y = -2000; }); // First shooting star after a short delay nextShootingStarAt = 3 + Math.random() * 5; initialized = true; animate(); } function resize() { const dpr = Math.min(window.devicePixelRatio || 1, 2); W = window.innerWidth; H = window.innerHeight; canvas.width = W * dpr; canvas.height = H * dpr; canvas.style.width = W + 'px'; canvas.style.height = H + 'px'; canvas.style.position = 'fixed'; canvas.style.top = '0'; canvas.style.left = '0'; ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.scale(dpr, dpr); initStars(); initShards(); } function debounce(fn, ms) { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; } function destroy() { if (rafId) cancelAnimationFrame(rafId); initialized = false; stars = []; shards = []; shootingStars = []; } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } window.HexBackground = { init, destroy }; })();