wa2k.com-Website/www/background.js
2026-05-08 22:13:32 -07:00

259 lines
8.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ═══════════════════════════════════════════════
// Bubble Background — wa2k.com
// Sparse frosted bubbles, slow drift, soft merge
// ═══════════════════════════════════════════════
(function () {
const canvas = document.createElement('canvas');
canvas.id = 'bubble-bg';
Object.assign(canvas.style, {
position: 'fixed',
top: '0',
left: '0',
width: '100%',
height: '100%',
zIndex: '-1',
pointerEvents: 'none',
});
document.body.prepend(canvas);
const ctx = canvas.getContext('2d');
// ── Config ──────────────────────────────────
const BG_COLOR = '#0a0e1a';
const BUBBLE_COUNT = 18; // sparse
const MIN_R = 28;
const MAX_R = 110;
const SPEED = 0.12; // very slow drift
const MERGE_DIST = 1.08; // merge when centers within 1.08× sum of radii
const MERGE_DURATION = 2800; // ms for merge animation
const SPLIT_DELAY = [6000, 14000]; // ms before a merged bubble splits
// Colour palette — pulled from site's glass theme
const COLORS = [
{ r: 196, g: 214, b: 226 }, // eww border blue-grey
{ r: 137, g: 180, b: 250 }, // primary neon
{ r: 166, g: 209, b: 255 }, // lighter blue
{ r: 180, g: 200, b: 220 }, // muted steel
];
// ── Resize ──────────────────────────────────
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();
// ── Bubble factory ──────────────────────────
let idSeq = 0;
function randColor() {
return COLORS[Math.floor(Math.random() * COLORS.length)];
}
function makeBubble(x, y, r) {
const angle = Math.random() * Math.PI * 2;
const speed = SPEED * (0.5 + Math.random() * 0.8);
const c = randColor();
return {
id: idSeq++,
x: x ?? Math.random() * canvas.width,
y: y ?? Math.random() * canvas.height,
r: r ?? MIN_R + Math.random() * (MAX_R - MIN_R),
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
c,
alpha: 0.13 + Math.random() * 0.10,
// merge state
merging: false,
mergeProgress: 0,
mergeTarget: null,
dead: false,
// split state
splitAt: null,
};
}
// ── Init bubbles ─────────────────────────────
let bubbles = [];
for (let i = 0; i < BUBBLE_COUNT; i++) bubbles.push(makeBubble());
// ── Merge helpers ────────────────────────────
function startMerge(a, b) {
// b merges into a; b dies after animation
a.merging = true;
a.mergeTarget = b;
a.mergeProgress = 0;
b.dead = true; // hide b immediately, a absorbs it
// Destination: midpoint, combined area radius
const combinedR = Math.min(MAX_R * 1.4, Math.sqrt(a.r * a.r + b.r * b.r));
a._mergeFrom = { x: a.x, y: a.y, r: a.r };
a._mergeTo = {
x: (a.x * a.r + b.x * b.r) / (a.r + b.r),
y: (a.y * a.r + b.y * b.r) / (a.r + b.r),
r: combinedR,
};
a._mergeDuration = MERGE_DURATION;
a._mergeStart = performance.now();
// Schedule split
const delay = SPLIT_DELAY[0] + Math.random() * (SPLIT_DELAY[1] - SPLIT_DELAY[0]);
a.splitAt = performance.now() + MERGE_DURATION + delay;
}
function finishMerge(a) {
a.x = a._mergeTo.x;
a.y = a._mergeTo.y;
a.r = a._mergeTo.r;
a.merging = false;
a.mergeTarget = null;
}
function splitBubble(a) {
// Split into two bubbles roughly original size
const childR = a.r / Math.sqrt(2);
const angle = Math.random() * Math.PI * 2;
const offset = childR * 0.6;
const b1 = makeBubble(a.x + Math.cos(angle) * offset, a.y + Math.sin(angle) * offset, childR);
const b2 = makeBubble(a.x - Math.cos(angle) * offset, a.y - Math.sin(angle) * offset, childR);
// Push apart
b1.vx = Math.cos(angle) * SPEED * 1.2;
b1.vy = Math.sin(angle) * SPEED * 1.2;
b2.vx = -b1.vx;
b2.vy = -b1.vy;
a.dead = true;
bubbles.push(b1, b2);
}
// ── Draw a single bubble ─────────────────────
function drawBubble(b, alpha) {
const { x, y, r, c } = b;
const a = (alpha ?? b.alpha);
// Outer glow
const glow = ctx.createRadialGradient(x, y, r * 0.5, x, y, r * 1.3);
glow.addColorStop(0, `rgba(${c.r},${c.g},${c.b},0)`);
glow.addColorStop(1, `rgba(${c.r},${c.g},${c.b},${(a * 0.18).toFixed(3)})`);
ctx.beginPath();
ctx.arc(x, y, r * 1.3, 0, Math.PI * 2);
ctx.fillStyle = glow;
ctx.fill();
// Body — frosted glass fill
const body = ctx.createRadialGradient(x - r * 0.3, y - r * 0.3, r * 0.05, x, y, r);
body.addColorStop(0, `rgba(${c.r},${c.g},${c.b},${(a * 0.22).toFixed(3)})`);
body.addColorStop(0.6, `rgba(${c.r},${c.g},${c.b},${(a * 0.08).toFixed(3)})`);
body.addColorStop(1, `rgba(${c.r},${c.g},${c.b},${(a * 0.03).toFixed(3)})`);
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fillStyle = body;
ctx.fill();
// Rim
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},${(a * 1.1).toFixed(3)})`;
ctx.lineWidth = 1.2;
ctx.stroke();
// Specular highlight — top-left catch
const spec = ctx.createRadialGradient(
x - r * 0.38, y - r * 0.38, 0,
x - r * 0.28, y - r * 0.28, r * 0.42
);
spec.addColorStop(0, `rgba(255,255,255,${(a * 1.4).toFixed(3)})`);
spec.addColorStop(1, 'rgba(255,255,255,0)');
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fillStyle = spec;
ctx.fill();
}
// ── Main loop ────────────────────────────────
let lastTime = performance.now();
function loop(now) {
const dt = Math.min(now - lastTime, 50); // cap dt
lastTime = now;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Background
ctx.fillStyle = BG_COLOR;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Remove dead bubbles
bubbles = bubbles.filter(b => !b.dead);
// Replenish if too few
while (bubbles.length < BUBBLE_COUNT) bubbles.push(makeBubble());
// Update & draw
for (let i = 0; i < bubbles.length; i++) {
const b = bubbles[i];
if (b.dead) continue;
// Split check
if (b.splitAt && now >= b.splitAt && !b.merging) {
splitBubble(b);
continue;
}
// Merge animation
if (b.merging) {
const t = Math.min(1, (now - b._mergeStart) / b._mergeDuration);
const ease = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; // ease in-out quad
b.x = b._mergeFrom.x + (b._mergeTo.x - b._mergeFrom.x) * ease;
b.y = b._mergeFrom.y + (b._mergeTo.y - b._mergeFrom.y) * ease;
b.r = b._mergeFrom.r + (b._mergeTo.r - b._mergeFrom.r) * ease;
if (t >= 1) finishMerge(b);
} else {
// Drift
b.x += b.vx * dt;
b.y += b.vy * dt;
// Soft wrap-around with margin
const m = b.r + 20;
if (b.x < -m) b.x = canvas.width + m;
if (b.x > canvas.width + m) b.x = -m;
if (b.y < -m) b.y = canvas.height + m;
if (b.y > canvas.height + m) b.y = -m;
// Very gentle direction wobble
b.vx += (Math.random() - 0.5) * 0.001;
b.vy += (Math.random() - 0.5) * 0.001;
// Clamp speed
const spd = Math.hypot(b.vx, b.vy);
const maxSpd = SPEED * 1.6;
if (spd > maxSpd) { b.vx *= maxSpd / spd; b.vy *= maxSpd / spd; }
// Check merge with other bubbles
for (let j = i + 1; j < bubbles.length; j++) {
const o = bubbles[j];
if (o.dead || o.merging || b.merging || b.splitAt || o.splitAt) continue;
const dx = o.x - b.x;
const dy = o.y - b.y;
const dist = Math.hypot(dx, dy);
if (dist < (b.r + o.r) * MERGE_DIST) {
// Larger absorbs smaller
if (b.r >= o.r) startMerge(b, o);
else startMerge(o, b);
break;
}
}
}
drawBubble(b);
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
})();