feat: implement Liquid Void interactive kinetic particle fluid as default background

This commit is contained in:
katR 2026-09-12 16:25:01 +05:30
parent 53a43b86f2
commit e35d497b5b

View file

@ -1257,7 +1257,7 @@
<b style="color:#ffffff">projects</b> - list active web projects & links
<b style="color:#ffffff">skills</b> - show competencies & skills
<b style="color:#ffffff">music</b> - music play | pause | vol &lt;0-100&gt;
<b style="color:#ffffff">bg &lt;mode&gt;</b> - change background (horizon, neural, matrix, video)
<b style="color:#ffffff">bg &lt;mode&gt;</b> - change background (liquid, horizon, matrix, video)
<b style="color:#ffffff">catpic</b> - fetch a fresh cat image
<b style="color:#ffffff">whoami</b> - print current session bio
<b style="color:#ffffff">clear</b> - clear terminal screen
@ -1341,12 +1341,12 @@
case 'bg':
case 'background':
if (arg === 'cyber' || arg === 'horizon') {
if (arg === 'liquid' || arg === 'void' || arg === 'fluid') {
setBgMode('liquid-void');
out.textContent = 'background set to: Liquid Void Fluid';
} else if (arg === 'cyber' || arg === 'horizon') {
setBgMode('cyber-horizon');
out.textContent = 'background set to: Cyber 3D Horizon';
} else if (arg === 'neural' || arg === 'stars' || arg === 'mesh') {
setBgMode('neural-mesh');
out.textContent = 'background set to: Neural Star-Mesh';
} else if (arg === 'matrix' || arg === 'rain') {
setBgMode('matrix-rain');
out.textContent = 'background set to: KatOS Digital Rain';
@ -1355,7 +1355,7 @@
out.textContent = 'background set to: Ambient Video';
} else {
cycleBgMode();
out.textContent = `background switched to: ${bgModes[currentBgModeIdx].name}\nusage: bg [horizon | neural | matrix | video]`;
out.textContent = `background switched to: ${bgModes[currentBgModeIdx].name}\nusage: bg [liquid | horizon | matrix | video]`;
}
break;
@ -1629,8 +1629,8 @@
}
const bgModes = [
{ id: 'liquid-void', name: 'Liquid Void Fluid' },
{ id: 'cyber-horizon', name: 'Cyber 3D Horizon' },
{ id: 'neural-mesh', name: 'Neural Star-Mesh' },
{ id: 'matrix-rain', name: 'KatOS Digital Rain' },
{ id: 'video-ambient', name: 'Ambient Video' }
];
@ -1669,12 +1669,14 @@
if (!silent) showToast(`[ BG: ${mode.name} ]`);
}
// Mouse Tracking with Smooth Lerp
// Interactive Mouse Tracking (Position & Kinetic Velocity)
const bgMouse = {
x: window.innerWidth / 2,
y: window.innerHeight / 2,
targetX: window.innerWidth / 2,
targetY: window.innerHeight / 2,
vx: 0,
vy: 0,
active: false
};
@ -1688,28 +1690,58 @@
bgMouse.active = false;
});
// Star / Constellation Node Pool
const STAR_COUNT = 85;
const stars = [];
function initStars() {
stars.length = 0;
// Gravitational Shockwaves on Click
const shockwaves = [];
window.addEventListener('mousedown', e => {
if (e.target.closest('.window-body') || e.target.closest('input') || e.target.closest('.player-btn')) return;
shockwaves.push({
x: e.clientX,
y: e.clientY,
radius: 10,
maxRadius: Math.max(window.innerWidth, window.innerHeight) * 0.45,
alpha: 0.65,
power: 1.2
});
});
// ── Liquid Void Kinetic Particle Fluid ──────────────────────────────────
const FLUID_PARTICLE_COUNT = 380;
const fluidParticles = [];
function initFluidParticles() {
fluidParticles.length = 0;
const W = bgCanvas.width;
const H = bgCanvas.height;
for (let i = 0; i < STAR_COUNT; i++) {
stars.push({
x: Math.random() * W,
y: Math.random() * H,
vx: (Math.random() - 0.5) * 0.35,
vy: (Math.random() - 0.5) * 0.3,
size: 0.8 + Math.random() * 1.8,
alpha: 0.2 + Math.random() * 0.65,
pulse: Math.random() * Math.PI * 2,
pulseSpeed: 0.02 + Math.random() * 0.03
for (let i = 0; i < FLUID_PARTICLE_COUNT; i++) {
const x = Math.random() * W;
const y = Math.random() * H;
fluidParticles.push({
x: x,
y: y,
oldX: x,
oldY: y,
vx: (Math.random() - 0.5) * 0.5,
vy: (Math.random() - 0.5) * 0.5,
speed: 0.85 + Math.random() * 1.3,
size: 0.7 + Math.random() * 1.7,
alpha: 0.22 + Math.random() * 0.65,
life: Math.random() * 500,
maxLife: 300 + Math.random() * 600
});
}
}
// Matrix Rain Columns
// Incompressible Curl-Noise Flow Field Generator (Zero Divergence)
function getCurlVector(x, y, t) {
const s1 = 0.0016;
const s2 = 0.0034;
const vx = -s1 * Math.sin(x * s1 + t * 0.00035) * Math.sin(y * s1 + t * 0.0003) + 0.45 * s2 * Math.cos((x + y) * s2 - t * 0.00045);
const vy = -(s1 * Math.cos(x * s1 + t * 0.00035) * Math.cos(y * s1 + t * 0.0003) + 0.45 * s2 * Math.cos((x + y) * s2 - t * 0.00045));
const len = Math.hypot(vx, vy) || 1;
return { vx: vx / len, vy: vy / len };
}
// Matrix Rain Setup
let mCols = 0;
let mDrops = [];
const mChars = '01KATOSOIUPOYTARCHLINUX7942#$><;:~^._.^';
@ -1721,13 +1753,13 @@
function resizeBgCanvas() {
bgCanvas.width = window.innerWidth;
bgCanvas.height = window.innerHeight;
initStars();
initFluidParticles();
initMatrixDrops();
}
window.addEventListener('resize', resizeBgCanvas);
resizeBgCanvas();
// Audio frequency extractor helper
// Audio frequency extractor
function getAudioBeat() {
if (!isPlaying || !analyser) return { bass: 0, mid: 0, energy: 0 };
const freqData = new Uint8Array(analyser.frequencyBinCount);
@ -1739,11 +1771,8 @@
return { bass, mid, energy: (bass * 0.65 + mid * 0.35) };
}
// Expanding shockwave pulse on bass hits
const shockwaves = [];
let lastBeatTime = 0;
let lastAudioBeat = 0;
let gridOffset = 0;
let radarAngle = 0;
function renderBgFrame(timestamp) {
requestAnimationFrame(renderBgFrame);
@ -1751,24 +1780,37 @@
const H = bgCanvas.height;
if (W === 0 || H === 0) return;
// Smooth mouse lerp
bgMouse.x += (bgMouse.targetX - bgMouse.x) * 0.08;
bgMouse.y += (bgMouse.targetY - bgMouse.y) * 0.08;
// Mouse velocity & position tracking
bgMouse.vx = (bgMouse.targetX - bgMouse.x);
bgMouse.vy = (bgMouse.targetY - bgMouse.y);
bgMouse.x += bgMouse.vx * 0.1;
bgMouse.y += bgMouse.vy * 0.1;
const audio = getAudioBeat();
// Trigger beat shockwave on intense bass
if (audio.bass > 0.65 && timestamp - lastBeatTime > 340) {
lastBeatTime = timestamp;
// Audio beat drop shockwave
if (audio.bass > 0.68 && timestamp - lastAudioBeat > 320) {
lastAudioBeat = timestamp;
shockwaves.push({
x: W / 2,
y: H * 0.65,
radius: 8,
maxRadius: Math.max(W, H) * 0.8,
alpha: 0.55
y: H / 2,
radius: 12,
maxRadius: Math.max(W, H) * 0.75,
alpha: 0.5,
power: 1.5
});
}
// Update shockwaves
for (let i = shockwaves.length - 1; i >= 0; i--) {
const sw = shockwaves[i];
sw.radius += 6.5;
sw.alpha *= 0.955;
if (sw.radius > sw.maxRadius || sw.alpha < 0.01) {
shockwaves.splice(i, 1);
}
}
const currentMode = bgModes[currentBgModeIdx].id;
if (currentMode === 'video-ambient') {
@ -1777,120 +1819,139 @@
}
if (currentMode === 'matrix-rain') {
// High-definition monochrome KatOS matrix rain
// High-definition monochrome matrix rain
bgCtx.fillStyle = 'rgba(0, 0, 0, 0.09)';
bgCtx.fillRect(0, 0, W, H);
bgCtx.font = '12px "JetBrains Mono", monospace';
for (let i = 0; i < mDrops.length; i++) {
const char = mChars.charAt(Math.floor(Math.random() * mChars.length));
const x = i * 18;
const y = mDrops[i] * 18;
if (y >= 0) {
bgCtx.fillStyle = '#ffffff';
bgCtx.shadowColor = '#ffffff';
bgCtx.shadowBlur = 8;
bgCtx.fillText(char, x, y);
bgCtx.shadowBlur = 0;
bgCtx.fillStyle = 'rgba(210, 210, 210, 0.45)';
const prevChar = mChars.charAt(Math.floor(Math.random() * mChars.length));
bgCtx.fillText(prevChar, x, y - 18);
}
if (y > H && Math.random() > 0.978) {
mDrops[i] = 0;
}
if (y > H && Math.random() > 0.978) mDrops[i] = 0;
mDrops[i]++;
}
return;
}
// Default modes: 'cyber-horizon' or 'neural-mesh'
bgCtx.fillStyle = 'rgba(0, 0, 0, 0.22)';
bgCtx.fillRect(0, 0, W, H);
if (currentMode === 'liquid-void') {
// ── "Liquid Void" Incompressible Kinetic Particle Fluid ─────────────
bgCtx.fillStyle = 'rgba(0, 0, 0, 0.14)';
bgCtx.fillRect(0, 0, W, H);
const isHorizon = (currentMode === 'cyber-horizon');
const horizonY = isHorizon ? H * 0.65 : H + 200;
// 1. Draw Starfield & Constellation Network
const activeStars = isHorizon ? stars.filter(s => s.y < horizonY + 25) : stars;
for (let i = 0; i < activeStars.length; i++) {
const s = activeStars[i];
s.x += s.vx * (1 + audio.bass * 0.5);
s.y += s.vy * (1 + audio.bass * 0.5);
s.pulse += s.pulseSpeed;
if (s.x < 0) s.x = W;
if (s.x > W) s.x = 0;
if (s.y < 0) s.y = isHorizon ? horizonY : H;
if (s.y > (isHorizon ? horizonY + 15 : H)) s.y = 0;
// Mouse gravity pull (subtle warp)
const dx = bgMouse.x - s.x;
const dy = bgMouse.y - s.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (bgMouse.active && dist < 140) {
const force = (1 - dist / 140) * 0.015;
s.x += dx * force;
s.y += dy * force;
// Glowing laser thread connecting cursor to node
bgCtx.strokeStyle = `rgba(255, 255, 255, ${(1 - dist / 140) * 0.32})`;
bgCtx.lineWidth = 1;
// Render expanding shockwaves
for (let i = 0; i < shockwaves.length; i++) {
const sw = shockwaves[i];
bgCtx.strokeStyle = `rgba(255, 255, 255, ${sw.alpha * 0.35})`;
bgCtx.lineWidth = 1.2;
bgCtx.beginPath();
bgCtx.moveTo(bgMouse.x, bgMouse.y);
bgCtx.lineTo(s.x, s.y);
bgCtx.arc(sw.x, sw.y, sw.radius, 0, Math.PI * 2);
bgCtx.stroke();
}
// Connections between neighboring stars
for (let j = i + 1; j < activeStars.length; j++) {
const s2 = activeStars[j];
const cdx = s.x - s2.x;
const cdy = s.y - s2.y;
const cDist = Math.sqrt(cdx * cdx + cdy * cdy);
if (cDist < 85) {
const lineAlpha = (1 - cDist / 85) * 0.16 * (1 + audio.mid * 0.8);
bgCtx.strokeStyle = `rgba(255, 255, 255, ${lineAlpha})`;
bgCtx.lineWidth = 0.8;
bgCtx.beginPath();
bgCtx.moveTo(s.x, s.y);
bgCtx.lineTo(s2.x, s2.y);
bgCtx.stroke();
const flowSpeed = (1.4 + audio.bass * 2.2);
for (let i = 0; i < fluidParticles.length; i++) {
const p = fluidParticles[i];
p.oldX = p.x;
p.oldY = p.y;
// 1. Natural Incompressible Curl Flow Field
const curl = getCurlVector(p.x, p.y, timestamp);
p.vx += curl.vx * 0.22 * p.speed;
p.vy += curl.vy * 0.22 * p.speed;
// 2. Interactive Mouse Vortex & Kinetic Attraction
if (bgMouse.active) {
const mdx = bgMouse.x - p.x;
const mdy = bgMouse.y - p.y;
const mDist = Math.hypot(mdx, mdy);
if (mDist < 220 && mDist > 0.001) {
const factor = (1 - mDist / 220);
// Tangential vortex whirlpool force
const swirlX = -mdy / mDist;
const swirlY = mdx / mDist;
p.vx += swirlX * factor * 2.8;
p.vy += swirlY * factor * 2.8;
// Gentle radial pull into the vortex center
p.vx += (mdx / mDist) * factor * 1.1;
p.vy += (mdy / mDist) * factor * 1.1;
// Kinetic velocity transfer from rapid cursor movement
p.vx += bgMouse.vx * factor * 0.18;
p.vy += bgMouse.vy * factor * 0.18;
}
}
}
// Draw star node
const glowBoost = audio.bass * 0.65;
const currentAlpha = Math.min(1, s.alpha * (0.8 + 0.3 * Math.sin(s.pulse)) + glowBoost);
bgCtx.fillStyle = `rgba(255, 255, 255, ${currentAlpha})`;
bgCtx.beginPath();
bgCtx.arc(s.x, s.y, s.size * (1 + glowBoost * 0.5), 0, Math.PI * 2);
bgCtx.fill();
// 3. Shockwave Impulse on particles
for (let k = 0; k < shockwaves.length; k++) {
const sw = shockwaves[k];
const sdx = p.x - sw.x;
const sdy = p.y - sw.y;
const sDist = Math.hypot(sdx, sdy);
const ringDist = Math.abs(sDist - sw.radius);
if (ringDist < 45 && sDist > 0.001) {
const push = (1 - ringDist / 45) * 3.5 * sw.alpha * sw.power;
p.vx += (sdx / sDist) * push;
p.vy += (sdy / sDist) * push;
}
}
// Fluid viscosity / drag
p.vx *= 0.92;
p.vy *= 0.92;
p.x += p.vx * flowSpeed;
p.y += p.vy * flowSpeed;
// Boundary wrapping with clean respawn
p.life++;
if (p.x < -10 || p.x > W + 10 || p.y < -10 || p.y > H + 10 || p.life > p.maxLife) {
p.x = Math.random() * W;
p.y = Math.random() * H;
p.oldX = p.x;
p.oldY = p.y;
p.vx = 0;
p.vy = 0;
p.life = 0;
}
// 4. Render Silky Luminous Trail
const vMag = Math.hypot(p.vx, p.vy);
const glowAlpha = Math.min(1, p.alpha * (0.8 + audio.bass * 0.9) + vMag * 0.08);
bgCtx.strokeStyle = `rgba(255, 255, 255, ${glowAlpha})`;
bgCtx.lineWidth = p.size * (1 + audio.bass * 0.45);
bgCtx.beginPath();
bgCtx.moveTo(p.oldX, p.oldY);
bgCtx.lineTo(p.x, p.y);
bgCtx.stroke();
// Bright stardust head point
bgCtx.fillStyle = `rgba(255, 255, 255, ${Math.min(1, glowAlpha + 0.3)})`;
bgCtx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size);
}
return;
}
// 2. Render expanding shockwaves
for (let i = shockwaves.length - 1; i >= 0; i--) {
const sw = shockwaves[i];
sw.radius += 5.5;
sw.alpha *= 0.96;
if (sw.radius > sw.maxRadius || sw.alpha < 0.01) {
shockwaves.splice(i, 1);
continue;
}
bgCtx.strokeStyle = `rgba(255, 255, 255, ${sw.alpha * 0.28})`;
bgCtx.lineWidth = 1.5;
bgCtx.beginPath();
bgCtx.ellipse(sw.x, sw.y, sw.radius, sw.radius * 0.35, 0, 0, Math.PI * 2);
bgCtx.stroke();
}
if (currentMode === 'cyber-horizon') {
// ── Clean 3D Perspective Horizon (Zero text or clutter) ──────────────
bgCtx.fillStyle = 'rgba(0, 0, 0, 0.22)';
bgCtx.fillRect(0, 0, W, H);
const horizonY = H * 0.65;
// 3. Render 3D Perspective Cyber-Grid (if in cyber-horizon mode)
if (isHorizon) {
// Horizon glow aura
// Horizon glow
const hGrad = bgCtx.createLinearGradient(0, horizonY - 35, 0, horizonY + 25);
hGrad.addColorStop(0, 'rgba(255, 255, 255, 0)');
hGrad.addColorStop(0.5, `rgba(255, 255, 255, ${0.1 + audio.bass * 0.2})`);
@ -1898,7 +1959,7 @@
bgCtx.fillStyle = hGrad;
bgCtx.fillRect(0, horizonY - 35, W, 60);
// Horizon sharp divider line
// Horizon sharp divider
bgCtx.strokeStyle = `rgba(255, 255, 255, ${0.4 + audio.bass * 0.4})`;
bgCtx.lineWidth = 1;
bgCtx.beginPath();
@ -1906,7 +1967,7 @@
bgCtx.lineTo(W, horizonY);
bgCtx.stroke();
// Longitudinal radiating perspective lines
// Longitudinal radiating lines
const lineCount = 28;
const centerX = W / 2;
const bottomSpread = W * 1.4;
@ -1929,8 +1990,6 @@
const progress = (k + gridOffset) / transLines;
const py = horizonY + (H - horizonY) * Math.pow(progress, 2.4);
const tAlpha = Math.min(0.35, progress * 0.38 + audio.bass * 0.15);
// Audio-reactive terrain ripple across the line
bgCtx.strokeStyle = `rgba(255, 255, 255, ${tAlpha})`;
bgCtx.beginPath();
const stepX = 20;