/* cosmic-bg.js — self-contained animated backgrounds for the fleet. * Drop in with: * Themes: starfield, cosmos, blackhole, geometric, nebula, particles, waves, grid * Config (data attrs): data-theme, data-density (low/med/high), data-speed (0.5-2) * All motion is slow + smooth by design. Visibility tuned to be clearly visible. */ (function () { "use strict"; var s = document.currentScript; var theme = (s && s.dataset.theme) || "cosmos"; var density = (s && s.dataset.density) || "med"; var speed = parseFloat((s && s.dataset.speed) || "1"); var D = density === "low" ? 0.5 : density === "high" ? 1.7 : 1; var c = document.createElement("canvas"); c.style.cssText = "position:fixed;inset:0;z-index:0;pointer-events:none;display:block"; function mount() { if (c.parentNode) return; (document.body || document.documentElement).appendChild(c); var st = document.createElement("style"); st.textContent = "body > *:not(canvas){position:relative;z-index:1}"; (document.head || document.documentElement).appendChild(st); } if (document.body) mount(); else document.addEventListener("DOMContentLoaded", mount); var ctx = c.getContext("2d"); var W, H, DPR = Math.min(window.devicePixelRatio || 1, 2); function resize() { W = window.innerWidth; H = window.innerHeight; c.width = W * DPR; c.height = H * DPR; c.style.width = W + "px"; c.style.height = H + "px"; ctx.setTransform(DPR, 0, 0, DPR, 0, 0); } resize(); window.addEventListener("resize", resize); function rnd(a, b) { return a + Math.random() * (b - a); } function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; } var PALETTES = { cosmic: ["#a06bff", "#5b8cff", "#ff5ab5", "#e8f0ff"], warm: ["#ff8a8a", "#ffd93d", "#ffb06a", "#fff"], cool: ["#4fd8ff", "#6fa8ff", "#bff3f2", "#fff"], green: ["#9fffd0", "#4de68a", "#c6ff7a", "#fff"], }; function hexA(hex, a) { var r = parseInt(hex.slice(1, 3), 16), g = parseInt(hex.slice(3, 5), 16), b = parseInt(hex.slice(5, 7), 16); return "rgba(" + r + "," + g + "," + b + "," + Math.max(0, Math.min(1, a)).toFixed(3) + ")"; } function starField(pal, drift) { var stars = []; var n = Math.floor(240 * D); for (var i = 0; i < n; i++) stars.push({ x: Math.random() * W, y: Math.random() * H, r: rnd(0.4, 2.1), tw: rnd(0, 6.28), tws: rnd(0.3, 1.2), dr: drift * rnd(0.3, 1), c: pick(pal) }); return function (dt, t) { for (var j = 0; j < stars.length; j++) { var st = stars[j]; st.y += st.dr; st.x += st.dr * 0.12; if (st.y > H + 4) { st.y = -4; st.x = Math.random() * W; } var a = (0.6 + 0.4 * Math.sin(t * st.tws + st.tw)) * (0.5 + 0.5 * st.r / 2.1); ctx.globalAlpha = a; ctx.fillStyle = st.c; ctx.beginPath(); ctx.arc(st.x, st.y, st.r, 0, 6.28); ctx.fill(); } ctx.globalAlpha = 1; }; } function nebulaBlobs(count, pal) { var blobs = []; for (var i = 0; i < count; i++) blobs.push({ x: Math.random() * W, y: Math.random() * H, r: rnd(140, 360), dx: rnd(-0.3, 0.3), dy: rnd(-0.2, 0.2), c: pick(pal), ph: rnd(0, 6.28) }); return function (dt, t) { for (var j = 0; j < blobs.length; j++) { var b = blobs[j]; b.x += b.dx; b.y += b.dy; if (b.x < -b.r) b.x = W + b.r; if (b.x > W + b.r) b.x = -b.r; if (b.y < -b.r) b.y = H + b.r; if (b.y > H + b.r) b.y = -b.r; var g = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, b.r); var pulse = 0.16 + 0.06 * Math.sin(t * 0.5 + b.ph); g.addColorStop(0, hexA(b.c, pulse)); g.addColorStop(1, hexA(b.c, 0)); ctx.fillStyle = g; ctx.fillRect(b.x - b.r, b.y - b.r, b.r * 2, b.r * 2); } }; } var themes = {}; themes.starfield = function () { var draw = starField(PALETTES.cosmic, 0.45); return function (dt, t) { draw(dt, t); }; }; themes.cosmos = function () { var stars = starField(PALETTES.cosmic, 0.35); var neb = nebulaBlobs(Math.floor(4 * D), PALETTES.cosmic); var arms = 3, particles = []; for (var i = 0; i < Math.floor(360 * D); i++) { var arm = Math.floor(Math.random() * arms); var rr = Math.pow(Math.random(), 0.6) * Math.min(W, H) * 0.44; var aa = (arm / arms) * 6.28 + rr * 0.02 + rnd(-0.2, 0.2); particles.push({ rr: rr, aa: aa, r: rnd(0.5, 1.8), c: pick(PALETTES.cosmic) }); } return function (dt, t) { neb(dt, t); var cx = W * 0.5, cy = H * 0.5; for (var j = 0; j < particles.length; j++) { var p = particles[j]; p.aa += 0.0016 * speed; var x = cx + Math.cos(p.aa) * p.rr, y = cy + Math.sin(p.aa) * p.rr; ctx.globalAlpha = 0.75; ctx.fillStyle = p.c; ctx.beginPath(); ctx.arc(x, y, p.r, 0, 6.28); ctx.fill(); } ctx.globalAlpha = 1; stars(dt, t); }; }; themes.blackhole = function () { var cx = W * 0.5, cy = H * 0.5; var bhR = Math.min(W, H) * 0.14; var acc = []; for (var i = 0; i < Math.floor(560 * D); i++) { var ang = rnd(0, 6.28), rad = rnd(bhR * 1.15, Math.min(W, H) * 0.5); acc.push({ ang: ang, rad: rad, speed: 0.006 + 0.016 * (bhR / rad), r: rnd(0.6, 2.2), c: pick(["#ffc06a", "#ff8a6a", "#ffd93d", "#ff5ab5", "#fff"]) }); } return function (dt, t) { // broad accretion glow var glow = ctx.createRadialGradient(cx, cy, bhR * 0.7, cx, cy, bhR * 3.6); glow.addColorStop(0, "rgba(255,150,60,0.5)"); glow.addColorStop(0.45, "rgba(255,45,149,0.22)"); glow.addColorStop(1, "rgba(0,0,0,0)"); ctx.fillStyle = glow; ctx.fillRect(0, 0, W, H); // particles spiralling in for (var j = 0; j < acc.length; j++) { var p = acc[j]; p.ang += p.speed * speed; p.rad -= 0.05 * speed; if (p.rad < bhR * 1.02) { p.rad = rnd(bhR * 1.5, Math.min(W, H) * 0.5); p.ang = rnd(0, 6.28); } var x = cx + Math.cos(p.ang) * p.rad, y = cy + Math.sin(p.ang) * p.rad; var stretch = Math.min(1, (p.rad - bhR) / (bhR * 1.6)); ctx.globalAlpha = 0.5 + 0.5 * stretch; ctx.fillStyle = p.c; ctx.beginPath(); ctx.arc(x, y, p.r * (0.5 + stretch), 0, 6.28); ctx.fill(); } ctx.globalAlpha = 1; // event horizon + bright photon ring ctx.fillStyle = "#000"; ctx.beginPath(); ctx.arc(cx, cy, bhR, 0, 6.28); ctx.fill(); var ring = ctx.createRadialGradient(cx, cy, bhR, cx, cy, bhR + 10); ring.addColorStop(0, "rgba(255,220,150,0.95)"); ring.addColorStop(1, "rgba(255,220,150,0)"); ctx.strokeStyle = "rgba(255,220,150,0.95)"; ctx.lineWidth = 2.5; ctx.beginPath(); ctx.arc(cx, cy, bhR + 1, 0, 6.28); ctx.stroke(); }; }; themes.geometric = function () { var polys = []; var n = Math.floor(22 * D); for (var i = 0; i < n; i++) polys.push({ x: Math.random() * W, y: Math.random() * H, sides: pick([3, 4, 6]), size: rnd(22, 60), rot: rnd(0, 6.28), vr: rnd(-0.0025, 0.0025), dx: rnd(-0.28, 0.28), dy: rnd(-0.22, 0.22), c: pick(PALETTES.cosmic) }); function poly(x, y, r, sides, rot) { ctx.beginPath(); for (var k = 0; k < sides; k++) { var a = rot + (k / sides) * 6.28; var px = x + Math.cos(a) * r, py = y + Math.sin(a) * r; k === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py); } ctx.closePath(); } return function (dt, t) { for (var j = 0; j < polys.length; j++) { var p = polys[j]; p.x += p.dx; p.y += p.dy; p.rot += p.vr; if (p.x < -70) p.x = W + 70; if (p.x > W + 70) p.x = -70; if (p.y < -70) p.y = H + 70; if (p.y > H + 70) p.y = -70; ctx.strokeStyle = hexA(p.c, 0.65); ctx.lineWidth = 1.4; poly(p.x, p.y, p.size, p.sides, p.rot); ctx.stroke(); ctx.fillStyle = hexA(p.c, 0.12); ctx.fill(); } for (var a = 0; a < polys.length; a++) { for (var b = a + 1; b < polys.length; b++) { var dx = polys[a].x - polys[b].x, dy = polys[a].y - polys[b].y; var d = Math.sqrt(dx * dx + dy * dy); if (d < 170) { ctx.strokeStyle = "rgba(190,200,255," + (0.22 * (1 - d / 170)).toFixed(3) + ")"; ctx.lineWidth = 0.8; ctx.beginPath(); ctx.moveTo(polys[a].x, polys[a].y); ctx.lineTo(polys[b].x, polys[b].y); ctx.stroke(); } } } }; }; themes.nebula = function () { var neb = nebulaBlobs(Math.floor(9 * D), PALETTES.cosmic.concat(PALETTES.cool)); var stars = starField(PALETTES.cosmic, 0.3); return function (dt, t) { neb(dt, t); stars(dt, t); }; }; themes.particles = function () { var ps = []; var n = Math.floor(130 * D); for (var i = 0; i < n; i++) ps.push({ x: Math.random() * W, y: Math.random() * H, vx: rnd(-0.35, 0.35), vy: rnd(-0.35, 0.35), r: rnd(1.2, 3), c: pick(PALETTES.cosmic) }); return function (dt, t) { for (var j = 0; j < ps.length; j++) { var p = ps[j]; p.x += p.vx; p.y += p.vy; if (p.x < 0 || p.x > W) p.vx *= -1; if (p.y < 0 || p.y > H) p.vy *= -1; ctx.fillStyle = p.c; ctx.globalAlpha = 0.95; ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, 6.28); ctx.fill(); } ctx.globalAlpha = 1; for (var a = 0; a < ps.length; a++) { for (var b = a + 1; b < ps.length; b++) { var dx = ps[a].x - ps[b].x, dy = ps[a].y - ps[b].y; var d = Math.sqrt(dx * dx + dy * dy); if (d < 150) { ctx.strokeStyle = "rgba(190,200,255," + (0.25 * (1 - d / 150)).toFixed(3) + ")"; ctx.lineWidth = 0.8; ctx.beginPath(); ctx.moveTo(ps[a].x, ps[a].y); ctx.lineTo(ps[b].x, ps[b].y); ctx.stroke(); } } } }; }; themes.waves = function () { var layers = [ { amp: 46, len: 260, speed: 0.010, y: 0.65, c: "rgba(160,80,255,0.30)" }, { amp: 60, len: 320, speed: 0.013, y: 0.72, c: "rgba(70,120,255,0.26)" }, { amp: 76, len: 400, speed: 0.016, y: 0.80, c: "rgba(255,70,170,0.22)" }, ]; return function (dt, t) { for (var j = 0; j < layers.length; j++) { var L = layers[j]; ctx.fillStyle = L.c; ctx.beginPath(); ctx.moveTo(0, H); for (var x = 0; x <= W; x += 8) { var y = H * L.y + Math.sin(x / L.len + t * L.speed * speed) * L.amp; ctx.lineTo(x, y); } ctx.lineTo(W, H); ctx.closePath(); ctx.fill(); } }; }; themes.grid = function () { var horizon = H * 0.55; return function (dt, t) { var off = (t * 30 * speed) % 40; ctx.strokeStyle = "rgba(150,80,255,0.45)"; ctx.lineWidth = 1.2; for (var i = -12; i <= 12; i++) { var x0 = W / 2 + i * 55; ctx.beginPath(); ctx.moveTo(W / 2, horizon); ctx.lineTo(x0, H); ctx.stroke(); } for (var j = 0; j < 16; j++) { var yy = horizon + Math.pow((j + off / 40) % 16 / 16, 2) * (H - horizon); ctx.strokeStyle = "rgba(255,70,170," + (0.10 + 0.35 * ((yy - horizon) / (H - horizon))).toFixed(3) + ")"; ctx.beginPath(); ctx.moveTo(0, yy); ctx.lineTo(W, yy); ctx.stroke(); } var g = ctx.createRadialGradient(W / 2, horizon, 0, W / 2, horizon, 170); g.addColorStop(0, "rgba(255,70,170,0.5)"); g.addColorStop(1, "rgba(0,0,0,0)"); ctx.fillStyle = g; ctx.fillRect(W / 2 - 170, horizon - 170, 340, 340); }; }; var active = themes[theme] ? themes[theme]() : themes.cosmos(); var last = performance.now(); var raf; function loop(now) { var dt = (now - last) / 1000; last = now; ctx.clearRect(0, 0, W, H); active(dt, now / 1000); raf = requestAnimationFrame(loop); } raf = requestAnimationFrame(loop); document.addEventListener("visibilitychange", function () { if (document.hidden) { cancelAnimationFrame(raf); } else { last = performance.now(); raf = requestAnimationFrame(loop); } }); })();