Snapshot: full project state
This commit is contained in:
303
static/cosmic-bg.js
Normal file
303
static/cosmic-bg.js
Normal file
@@ -0,0 +1,303 @@
|
||||
/* cosmic-bg.js — self-contained animated backgrounds for the fleet.
|
||||
* Drop in with: <script src="cosmic-bg.js" data-theme="blackhole"></script>
|
||||
* 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); }
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user