feat: implement Liquid Void interactive kinetic particle fluid as default background
This commit is contained in:
parent
53a43b86f2
commit
e35d497b5b
1 changed files with 185 additions and 126 deletions
311
index.html
311
index.html
|
|
@ -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 <0-100>
|
||||
<b style="color:#ffffff">bg <mode></b> - change background (horizon, neural, matrix, video)
|
||||
<b style="color:#ffffff">bg <mode></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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue