diff --git a/index.html b/index.html index 437e34a..4c13ca9 100644 --- a/index.html +++ b/index.html @@ -1257,7 +1257,7 @@ projects - list active web projects & links skills - show competencies & skills music - music play | pause | vol <0-100> - bg <mode> - change background (horizon, neural, matrix, video) + bg <mode> - change background (liquid, horizon, matrix, video) catpic - fetch a fresh cat image whoami - print current session bio clear - 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;