@@ -1457,12 +1547,156 @@
} else if (e.key === 'Tab') {
e.preventDefault();
const cur = termInput.value.trim().toLowerCase();
- const available = ['help', 'fastfetch', 'neofetch', 'clear', 'cat', 'ls', 'projects', 'skills', 'music', 'matrix', 'whoami', 'date', 'reboot'];
+ const available = ['help', 'fastfetch', 'neofetch', 'clear', 'cat', 'cowsay', 'fortune', 'sl', 'cmatrix', 'pet', 'feed', 'ls', 'projects', 'skills', 'music', 'matrix', 'whoami', 'date', 'reboot'];
const match = available.find(c => c.startsWith(cur));
if (match) termInput.value = match;
}
});
+ // ── Desktop ASCII Cat Companion Interaction & Quotes ─────────────────────
+ const katPetAscii = document.getElementById('katPetAscii');
+ const katPetBubble = document.getElementById('katPetBubble');
+ let katBubbleTimer = null;
+ const katPhrases = [
+ "purrrrrrrr 🤍",
+ "mrrp! :3",
+ "cats > people haha",
+ "i use arch btw",
+ "you da real kat 🤍",
+ "*happy headbonk*",
+ "feed me fish! (type 'feed cat')",
+ "*bobs head to music*",
+ "minecraft shyt & linux",
+ "sudo give pets"
+ ];
+
+ function interactKatPet(action = 'click') {
+ if (!katPetAscii || !katPetBubble) return;
+ clearTimeout(katBubbleTimer);
+
+ if (action === 'feed') {
+ katPetAscii.textContent = '/ᐠ. ڡ .ᐟ\\ 🐟';
+ katPetBubble.textContent = '*nom nom* yummy fish! 🤍';
+ } else if (action === 'pet') {
+ katPetAscii.textContent = '/ᐠ˶> ᆺ <˶ᐟ\\ 🤍';
+ katPetBubble.textContent = 'purrrrrrrr... 🤍';
+ } else {
+ const phrase = katPhrases[Math.floor(Math.random() * katPhrases.length)];
+ katPetAscii.textContent = '/ᐠ˶> ᆺ <˶ᐟ\\';
+ katPetBubble.textContent = phrase;
+ }
+
+ katPetBubble.classList.add('show');
+ showToast('[ kat: purr 🤍 ]');
+
+ katBubbleTimer = setTimeout(() => {
+ katPetBubble.classList.remove('show');
+ katPetAscii.textContent = '/ᐠ. ᆺ .ᐟ\\';
+ }, 2600);
+ }
+
+ // ── Linux Fortunes & Cowsay Generator ────────────────────────────────────
+ const katFortunes = [
+ "Arch Linux: because installing an OS in 40 manual commands builds character.",
+ "A cat has 9 lives. A Linux kernel panic has infinite retries.",
+ "There are 10 types of people: those who understand binary, and cats who sit on keyboards.",
+ "Minecraft server status: running out of RAM. Solution: fed the cat instead.",
+ "Hyprland config broke at 3 AM. Worth it for the 1px obsidian border.",
+ "sudo rm -rf / --no-preserve-root : The cat walked across the keyboard again.",
+ "Fastfetch is not just a tool; it's an obsidian lifestyle.",
+ "Real hackers don't sleep, they just wait for pacman -Syu to finish.",
+ "Cats > People. This has been mathematically proven by Arch Linux.",
+ "Self-hosting everything because trusting the cloud is a vulnerability.",
+ "Your CPU is currently 99.8% cooler because katOS is running.",
+ "i cant stop loving linux, pls speed i need this."
+ ];
+
+ function getRandomFortune() {
+ return katFortunes[Math.floor(Math.random() * katFortunes.length)];
+ }
+
+ function formatCowsay(msg) {
+ const cleanMsg = (msg || 'meow').replace(/[\r\n]+/g, ' ').trim();
+ const maxLen = 40;
+ const wrapped = [];
+ let words = cleanMsg.split(' ');
+ let curLine = '';
+ for (let w of words) {
+ if ((curLine + ' ' + w).trim().length > maxLen) {
+ if (curLine) wrapped.push(curLine);
+ curLine = w;
+ } else {
+ curLine = (curLine ? curLine + ' ' : '') + w;
+ }
+ }
+ if (curLine) wrapped.push(curLine);
+ if (wrapped.length === 0) wrapped.push('meow');
+
+ const width = Math.max(...wrapped.map(l => l.length));
+ const borderTop = ' ' + '_'.repeat(width + 2);
+ const borderBot = ' ' + '-'.repeat(width + 2);
+ const body = wrapped.map((line, idx) => {
+ const pad = ' '.repeat(width - line.length);
+ if (wrapped.length === 1) return `< ${line} >`;
+ if (idx === 0) return `/ ${line}${pad} \\`;
+ if (idx === wrapped.length - 1) return `\\ ${line}${pad} /`;
+ return `| ${line}${pad} |`;
+ }).join('\n');
+
+ return `${borderTop}\n${body}\n${borderBot}\n \\ ^__^\n \\ (oo)\\_______\n (__)\\ )\\/\\\n ||----w |\n || ||`;
+ }
+
+ // ── Classic "sl" Steam Locomotive Easter Egg ──────────────────────────────
+ function runSteamLocomotive() {
+ const trainContainer = document.createElement('pre');
+ trainContainer.className = 'sl-train';
+ trainContainer.style.cssText = 'font-family:monospace; line-height:1.15; color:#ffffff; text-shadow:var(--glow-subtle); white-space:pre; margin:8px 0; overflow:hidden; font-size:10px;';
+ termHistory.appendChild(trainContainer);
+ termInput.disabled = true;
+
+ const trainArt = [
+ ` ( ) (@@) ( ) (@) () @@ O`,
+ ` (@@@)`,
+ ` ( )`,
+ ` (@@@@)`,
+ ` ( )`,
+ ` ==== ________ ___________`,
+ ` _D _| |_______/ \\__I_I_____===__|_________|`,
+ ` |(_)--- | H\\________/ | | =|___ ___|`,
+ ` / | | H | | | | | |`,
+ ` | | | H |__--------------------| [katOS] |`,
+ ` | ________|___H__/__|_____/[ ] \\___|____|_________|`,
+ ` (O)____(O) [ ] (O)(O) (O) (O) (O)(O)`
+ ];
+
+ let offset = 42;
+ const interval = setInterval(() => {
+ if (offset < -68) {
+ clearInterval(interval);
+ trainContainer.remove();
+ termInput.disabled = false;
+ termInput.focus();
+ const doneRow = document.createElement('div');
+ doneRow.className = 'term-output';
+ doneRow.innerHTML = `
sl: Steam Locomotive chugged past oiupoyt@archlinux!`;
+ termHistory.appendChild(doneRow);
+ termBody.scrollTop = termBody.scrollHeight;
+ return;
+ }
+
+ const indent = Math.max(0, offset);
+ const padding = ' '.repeat(indent);
+ const sliceAmt = offset < 0 ? Math.abs(offset) : 0;
+
+ trainContainer.textContent = trainArt
+ .map(line => padding + (sliceAmt > 0 ? line.slice(sliceAmt) : line))
+ .join('\n');
+
+ offset -= 3;
+ termBody.scrollTop = termBody.scrollHeight;
+ }, 50);
+ }
+
function execCommand(cmdLine) {
const parts = cmdLine.split(' ');
const cmd = parts[0].toLowerCase();
@@ -1484,6 +1718,11 @@
case 'help':
out.innerHTML = `Available commands:
fastfetch - display Arch Linux logo & system specs
+
cowsay <msg> - ASCII cow speaks your text (or fortune)
+
fortune - random funny Linux, Arch, or cat fortune
+
sl - classic steam locomotive runs across terminal
+
cmatrix - stream falling KatOS digital rain
+
pet / feed - pet or feed your desktop kat companion
ls - list virtual directory contents
cat <file> - display contents of a file (e.g. cat about.md)
projects - list active web projects & links
@@ -1504,13 +1743,52 @@
out.textContent = '';
break;
+ case 'cowsay':
+ const cowMsg = arg.trim() || getRandomFortune();
+ out.innerHTML = `
${escapeHtml(formatCowsay(cowMsg))}`;
+ break;
+
+ case 'fortune':
+ const fort = getRandomFortune();
+ out.innerHTML = `
"${escapeHtml(fort)}"`;
+ break;
+
+ case 'sl':
+ runSteamLocomotive();
+ return;
+
+ case 'cmatrix':
+ setBgMode('matrix-rain');
+ out.innerHTML = `
[ CMATRIX PROTOCOL ENGAGED ]\n"Wake up, oiupoyt... The matrix has you."\nKatOS digital rain is now cascading.\nType
bg liquid to return to Liquid Void.`;
+ break;
+
+ case 'pet':
+ case 'petcat':
+ case 'pet-cat':
+ interactKatPet('pet');
+ out.innerHTML = `
/ᐠ˶> ᆺ <˶ᐟ\\ *purrrrrrrrr* 🤍\nYou petted the desktop kat! Kat is purring happily.`;
+ break;
+
+ case 'feed':
+ case 'feedcat':
+ case 'feed-cat':
+ interactKatPet('feed');
+ out.innerHTML = `
/ᐠ. ڡ .ᐟ\\ *nom nom nom* 🐟\nYou fed the desktop kat a fresh fish! (+20 kat affinity)`;
+ break;
+
case 'ls':
out.innerHTML = `
about.md skillz.bash websites.html links.zip music.mp3 backgroundVideo.mp4`;
break;
case 'cat':
if (!arg) {
- out.textContent = 'cat: missing file operand. try: cat about.md';
+ out.textContent = 'cat: missing file operand. try: cat about.md (or: pet cat, feed cat, catpic)';
+ } else if (arg.includes('pet')) {
+ interactKatPet('pet');
+ out.innerHTML = `
/ᐠ˶> ᆺ <˶ᐟ\\ *purrrrrrrrr* 🤍\nYou petted the desktop kat! Kat is purring happily.`;
+ } else if (arg.includes('feed')) {
+ interactKatPet('feed');
+ out.innerHTML = `
/ᐠ. ڡ .ᐟ\\ *nom nom nom* 🐟\nYou fed the desktop kat a fresh fish! (+20 kat affinity)`;
} else if (arg.includes('about')) {
out.textContent = `i cant stop loving linux, pls speed i need this\nminecraft shyt, networking experiments, and linux customization.\ni was on the ███████████████\ncats > people haha.\ni use arch yea\nyou da real kat 🤍.`;
} else if (arg.includes('skill')) {
@@ -1558,7 +1836,6 @@
case 'catpic':
case 'cat-image':
- case 'cat':
out.innerHTML = `Fetching cat...`;
fetch('https://api.thecatapi.com/v1/images/search')
.then(r => r.json())
@@ -1994,14 +2271,14 @@
});
// ── 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 < FLUID_PARTICLE_COUNT; i++) {
+ const particleCount = (window.innerWidth <= 800) ? 120 : 280;
+ for (let i = 0; i < particleCount; i++) {
const x = Math.random() * W;
const y = Math.random() * H;
fluidParticles.push({
@@ -2009,11 +2286,11 @@
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,
+ vx: (Math.random() - 0.5) * 0.4,
+ vy: (Math.random() - 0.5) * 0.4,
+ speed: 0.85 + Math.random() * 1.2,
+ size: 0.8 + Math.random() * 1.5,
+ alpha: 0.3 + Math.random() * 0.6,
life: Math.random() * 500,
maxLife: 300 + Math.random() * 600
});
@@ -2022,12 +2299,9 @@
// Incompressible Curl-Noise Flow Field Generator (Zero Allocation & Fast Math)
let curlVx = 0, curlVy = 0;
- function computeCurl(x, y, t) {
+ function computeCurl(x, y, t1, t2, t3) {
const s1 = 0.0016;
const s2 = 0.0034;
- const t1 = t * 0.00035;
- const t2 = t * 0.0003;
- const t3 = t * 0.00045;
const vx = -s1 * Math.sin(x * s1 + t1) * Math.sin(y * s1 + t2) + 0.45 * s2 * Math.cos((x + y) * s2 - t3);
const vy = -(s1 * Math.cos(x * s1 + t1) * Math.sin(y * s1 + t2) + 0.45 * s2 * Math.cos((x + y) * s2 - t3));
const invLen = 1 / (Math.sqrt(vx * vx + vy * vy) || 1);
@@ -2091,6 +2365,15 @@
const audio = getAudioBeat();
+ // Reactive Kat Pet bobbing with audio bass beat
+ if (katPetAscii) {
+ if (audio.bass > 0.62) {
+ katPetAscii.style.transform = 'translateY(-2.5px) scale(1.08)';
+ } else {
+ katPetAscii.style.transform = 'none';
+ }
+ }
+
// Audio beat drop shockwave
if (audio.bass > 0.68 && timestamp - lastAudioBeat > 320) {
lastAudioBeat = timestamp;
@@ -2147,7 +2430,7 @@
}
if (currentMode === 'liquid-void') {
- // ── "Liquid Void" Incompressible Kinetic Particle Fluid ─────────────
+ // ── "Liquid Void" Incompressible Kinetic Particle Fluid (Hyper-Optimized 60fps) ──
bgCtx.fillStyle = 'rgba(0, 0, 0, 0.14)';
bgCtx.fillRect(0, 0, W, H);
@@ -2162,18 +2445,24 @@
}
const flowSpeed = (1.4 + audio.bass * 2.2);
+ const isMobile = window.innerWidth <= 800;
+ const t1 = timestamp * 0.00035;
+ const t2 = timestamp * 0.0003;
+ const t3 = timestamp * 0.00045;
+ const numShock = shockwaves.length;
+ // Pass 1: Physics & Movement
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 (Zero Allocation)
- computeCurl(p.x, p.y, timestamp);
+ // 1. Natural Incompressible Curl Flow Field (Precalculated Time Factors)
+ computeCurl(p.x, p.y, t1, t2, t3);
p.vx += curlVx * 0.22 * p.speed;
p.vy += curlVy * 0.22 * p.speed;
- // 2. Interactive Mouse Vortex & Kinetic Attraction
+ // 2. Interactive Mouse / Touch Vortex
if (bgMouse.active) {
const mdx = bgMouse.x - p.x;
const mdy = bgMouse.y - p.y;
@@ -2183,34 +2472,36 @@
const factor = (1 - mDist * 0.00454545);
const invMDist = 1 / mDist;
- // Tangential vortex whirlpool force
const swirlX = -mdy * invMDist;
const swirlY = mdx * invMDist;
p.vx += swirlX * factor * 2.8;
p.vy += swirlY * factor * 2.8;
- // Gentle radial pull into the vortex center
p.vx += (mdx * invMDist) * factor * 1.1;
p.vy += (mdy * invMDist) * factor * 1.1;
- // Kinetic velocity transfer from rapid cursor movement
p.vx += bgMouse.vx * factor * 0.18;
p.vy += bgMouse.vy * factor * 0.18;
}
}
- // 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.sqrt(sdx * sdx + sdy * sdy);
- const ringDist = Math.abs(sDist - sw.radius);
- if (ringDist < 45 && sDist > 0.001) {
- const push = (1 - ringDist * 0.02222) * 3.5 * sw.alpha * sw.power;
- const invSDist = 1 / sDist;
- p.vx += (sdx * invSDist) * push;
- p.vy += (sdy * invSDist) * push;
+ // 3. Shockwave Impulse (Squared Distance early rejection)
+ if (numShock > 0) {
+ for (let k = 0; k < numShock; k++) {
+ const sw = shockwaves[k];
+ const sdx = p.x - sw.x;
+ const sdy = p.y - sw.y;
+ const sDistSq = sdx * sdx + sdy * sdy;
+ const rMin = Math.max(0, sw.radius - 45);
+ const rMax = sw.radius + 45;
+ if (sDistSq >= rMin * rMin && sDistSq <= rMax * rMax) {
+ const sDist = Math.sqrt(sDistSq);
+ const ringDist = Math.abs(sDist - sw.radius);
+ const push = (1 - ringDist * 0.02222) * 3.5 * sw.alpha * sw.power;
+ const invSDist = 1 / sDist;
+ p.vx += (sdx * invSDist) * push;
+ p.vy += (sdy * invSDist) * push;
+ }
}
}
@@ -2232,22 +2523,27 @@
p.vy = 0;
p.life = 0;
}
+ }
- // 4. Render Silky Luminous Trail
- const vMag = Math.sqrt(p.vx * p.vx + p.vy * 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();
+ // BATCH PASS 1: Stream line trails in a single GPU draw call
+ bgCtx.beginPath();
+ for (let i = 0; i < fluidParticles.length; i++) {
+ const p = fluidParticles[i];
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);
}
+ bgCtx.lineWidth = isMobile ? 1.1 : (1.2 + audio.bass * 0.45);
+ bgCtx.strokeStyle = isMobile ? 'rgba(255, 255, 255, 0.48)' : `rgba(255, 255, 255, ${0.42 + audio.bass * 0.35})`;
+ bgCtx.stroke();
+
+ // BATCH PASS 2: Glowing particle heads
+ bgCtx.fillStyle = 'rgba(255, 255, 255, 0.88)';
+ for (let i = 0; i < fluidParticles.length; i++) {
+ const p = fluidParticles[i];
+ const sz = isMobile ? 1.5 : p.size;
+ bgCtx.fillRect(p.x - sz * 0.5, p.y - sz * 0.5, sz, sz);
+ }
+
return;
}