Keep the cat in the navbar !

Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
Hadi
2026-04-24 23:20:58 +02:00
parent e332a5732b
commit 97bdfd9a6e
2 changed files with 99 additions and 240 deletions
+97 -240
View File
@@ -1,284 +1,141 @@
// oneko.js: https://github.com/adryd325/oneko.js // oneko.js — navbar wanderer (based on https://github.com/adryd325/oneko.js)
(function oneko() { (function oneko() {
const isReducedMotion = if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
window.matchMedia(`(prefers-reduced-motion: reduce)`) === true ||
window.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true;
if (isReducedMotion) return; const SIZE = 32;
const SPEED = 10;
const nekoEl = document.createElement("div");
let persistPosition = true;
let nekoPosX = 32;
let nekoPosY = 32;
let mousePosX = 0;
let mousePosY = 0;
let frameCount = 0;
let idleTime = 0;
let idleAnimation = null;
let idleAnimationFrame = 0;
const nekoSpeed = 10;
const spriteSets = { const spriteSets = {
idle: [[-3, -3]], idle: [[-3, -3]],
alert: [[-7, -3]], alert: [[-7, -3]],
scratchSelf: [ scratchSelf: [[-5, 0], [-6, 0], [-7, 0]],
[-5, 0], scratchWallE:[[-2, -2], [-2, -3]],
[-6, 0], scratchWallW:[[-4, 0], [-4, -1]],
[-7, 0], tired: [[-3, -2]],
], sleeping: [[-2, 0], [-2, -1]],
scratchWallN: [ E: [[-3, 0], [-3, -1]],
[0, 0], W: [[-4, -2], [-4, -3]],
[0, -1],
],
scratchWallS: [
[-7, -1],
[-6, -2],
],
scratchWallE: [
[-2, -2],
[-2, -3],
],
scratchWallW: [
[-4, 0],
[-4, -1],
],
tired: [[-3, -2]],
sleeping: [
[-2, 0],
[-2, -1],
],
N: [
[-1, -2],
[-1, -3],
],
NE: [
[0, -2],
[0, -3],
],
E: [
[-3, 0],
[-3, -1],
],
SE: [
[-5, -1],
[-5, -2],
],
S: [
[-6, -3],
[-7, -2],
],
SW: [
[-5, -3],
[-6, -1],
],
W: [
[-4, -2],
[-4, -3],
],
NW: [
[-1, 0],
[-1, -1],
],
}; };
function init() { const track = document.getElementById("oneko-track");
let nekoFile = "/oneko.gif"; if (!track) return;
const curScript = document.currentScript;
if (curScript && curScript.dataset.cat) {
nekoFile = curScript.dataset.cat;
}
if (curScript && curScript.dataset.persistPosition) {
if (curScript.dataset.persistPosition === "") {
persistPosition = true;
} else {
persistPosition = JSON.parse(
curScript.dataset.persistPosition.toLowerCase(),
);
}
}
if (persistPosition) { const el = document.createElement("div");
let storedNeko = JSON.parse(window.localStorage.getItem("oneko")); el.id = "oneko";
if (storedNeko !== null) { el.ariaHidden = "true";
nekoPosX = storedNeko.nekoPosX; el.style.width = `${SIZE}px`;
nekoPosY = storedNeko.nekoPosY; el.style.height = `${SIZE}px`;
mousePosX = storedNeko.mousePosX; el.style.position = "absolute";
mousePosY = storedNeko.mousePosY; el.style.bottom = "0";
frameCount = storedNeko.frameCount; el.style.pointerEvents = "none";
idleTime = storedNeko.idleTime; el.style.imageRendering = "pixelated";
idleAnimation = storedNeko.idleAnimation; el.style.zIndex = "2147483647";
idleAnimationFrame = storedNeko.idleAnimationFrame; el.style.backgroundImage= "url(/oneko.gif)";
nekoEl.style.backgroundPosition = storedNeko.bgPos; track.appendChild(el);
}
}
nekoEl.id = "oneko"; function maxX() { return track.offsetWidth - SIZE; }
nekoEl.ariaHidden = true; function clamp(v,lo,hi) { return Math.max(lo, Math.min(hi, v)); }
nekoEl.style.width = "32px"; function randomTarget() { return Math.random() * maxX(); }
nekoEl.style.height = "32px";
nekoEl.style.position = "fixed";
nekoEl.style.pointerEvents = "none";
nekoEl.style.imageRendering = "pixelated";
nekoEl.style.left = `${nekoPosX - 16}px`;
nekoEl.style.top = `${nekoPosY - 16}px`;
nekoEl.style.zIndex = 2147483647;
nekoEl.style.backgroundImage = `url(${nekoFile})`;
document.body.appendChild(nekoEl);
document.addEventListener("mousemove", function (event) {
mousePosX = event.clientX;
mousePosY = event.clientY;
});
if (persistPosition) {
window.addEventListener("beforeunload", function (_event) {
window.localStorage.setItem(
"oneko",
JSON.stringify({
nekoPosX: nekoPosX,
nekoPosY: nekoPosY,
mousePosX: mousePosX,
mousePosY: mousePosY,
frameCount: frameCount,
idleTime: idleTime,
idleAnimation: idleAnimation,
idleAnimationFrame: idleAnimationFrame,
bgPos: nekoEl.style.backgroundPosition,
}),
);
});
}
window.requestAnimationFrame(onAnimationFrame);
}
let lastFrameTimestamp;
function onAnimationFrame(timestamp) {
// Stops execution if the neko element is removed from DOM
if (!nekoEl.isConnected) {
return;
}
if (!lastFrameTimestamp) {
lastFrameTimestamp = timestamp;
}
if (timestamp - lastFrameTimestamp > 100) {
lastFrameTimestamp = timestamp;
frame();
}
window.requestAnimationFrame(onAnimationFrame);
}
function setSprite(name, frame) { function setSprite(name, frame) {
const sprite = spriteSets[name][frame % spriteSets[name].length]; const s = spriteSets[name][frame % spriteSets[name].length];
nekoEl.style.backgroundPosition = `${sprite[0] * 32}px ${sprite[1] * 32}px`; el.style.backgroundPosition = `${s[0] * SIZE}px ${s[1] * SIZE}px`;
} }
function resetIdleAnimation() { let posX = randomTarget();
idleAnimation = null; let targetX = posX;
idleAnimationFrame = 0; el.style.left = `${posX}px`;
}
let frameCount = 0;
let idleTime = 0;
let idleAnim = null;
let idleAnimFrame = 0;
let lastTs = null;
function resetIdle() { idleAnim = null; idleAnimFrame = 0; }
function idle() { function idle() {
idleTime += 1; idleTime++;
// every ~ 20 seconds if (idleTime > 40 && Math.floor(Math.random() * 120) === 0) {
if ( targetX = randomTarget();
idleTime > 10 && idleTime = 0;
Math.floor(Math.random() * 200) == 0 && resetIdle();
idleAnimation == null return;
) {
let avalibleIdleAnimations = ["sleeping", "scratchSelf"];
if (nekoPosX < 32) {
avalibleIdleAnimations.push("scratchWallW");
}
if (nekoPosY < 32) {
avalibleIdleAnimations.push("scratchWallN");
}
if (nekoPosX > window.innerWidth - 32) {
avalibleIdleAnimations.push("scratchWallE");
}
if (nekoPosY > window.innerHeight - 32) {
avalibleIdleAnimations.push("scratchWallS");
}
idleAnimation =
avalibleIdleAnimations[
Math.floor(Math.random() * avalibleIdleAnimations.length)
];
} }
switch (idleAnimation) { if (idleTime > 15 && idleAnim == null && Math.floor(Math.random() * 180) === 0) {
const opts = ["sleeping", "scratchSelf"];
if (posX <= SIZE) opts.push("scratchWallW");
if (posX >= maxX() - SIZE) opts.push("scratchWallE");
idleAnim = opts[Math.floor(Math.random() * opts.length)];
}
switch (idleAnim) {
case "sleeping": case "sleeping":
if (idleAnimationFrame < 8) { if (idleAnimFrame < 8) { setSprite("tired", 0); break; }
setSprite("tired", 0); setSprite("sleeping", Math.floor(idleAnimFrame / 4));
break; if (idleAnimFrame > 192) resetIdle();
}
setSprite("sleeping", Math.floor(idleAnimationFrame / 4));
if (idleAnimationFrame > 192) {
resetIdleAnimation();
}
break; break;
case "scratchWallN":
case "scratchWallS":
case "scratchWallE": case "scratchWallE":
case "scratchWallW": case "scratchWallW":
case "scratchSelf": case "scratchSelf":
setSprite(idleAnimation, idleAnimationFrame); setSprite(idleAnim, idleAnimFrame);
if (idleAnimationFrame > 9) { if (idleAnimFrame > 9) resetIdle();
resetIdleAnimation();
}
break; break;
default: default:
setSprite("idle", 0); setSprite("idle", 0);
return; return;
} }
idleAnimationFrame += 1; idleAnimFrame++;
} }
function frame() { function frame() {
frameCount += 1; frameCount++;
const diffX = nekoPosX - mousePosX; const diff = targetX - posX;
const diffY = nekoPosY - mousePosY; const dist = Math.abs(diff);
const distance = Math.sqrt(diffX ** 2 + diffY ** 2);
if (distance < nekoSpeed || distance < 48) { if (dist < SPEED) {
posX = targetX;
idle(); idle();
return; return;
} }
idleAnimation = null; resetIdle();
idleAnimationFrame = 0;
if (idleTime > 1) { if (idleTime > 5) {
setSprite("alert", 0); setSprite("alert", 0);
// count down after being alerted before moving idleTime = Math.min(idleTime, 7) - 1;
idleTime = Math.min(idleTime, 7);
idleTime -= 1;
return; return;
} }
idleTime = 0;
let direction; setSprite(diff > 0 ? "E" : "W", frameCount);
direction = diffY / distance > 0.5 ? "N" : ""; posX = clamp(posX + (diff > 0 ? SPEED : -SPEED), 0, maxX());
direction += diffY / distance < -0.5 ? "S" : ""; el.style.left = `${posX}px`;
direction += diffX / distance > 0.5 ? "W" : "";
direction += diffX / distance < -0.5 ? "E" : "";
setSprite(direction, frameCount);
nekoPosX -= (diffX / distance) * nekoSpeed;
nekoPosY -= (diffY / distance) * nekoSpeed;
nekoPosX = Math.min(Math.max(16, nekoPosX), window.innerWidth - 16);
nekoPosY = Math.min(Math.max(16, nekoPosY), window.innerHeight - 16);
nekoEl.style.left = `${nekoPosX - 16}px`;
nekoEl.style.top = `${nekoPosY - 16}px`;
} }
init(); function loop(ts) {
if (!el.isConnected) return;
if (!lastTs) lastTs = ts;
if (ts - lastTs > 100) { lastTs = ts; frame(); }
window.requestAnimationFrame(loop);
}
let lastTrackWidth = track.offsetWidth;
window.addEventListener("resize", () => {
const newWidth = track.offsetWidth;
if (lastTrackWidth > 0) {
const ratio = newWidth / lastTrackWidth;
posX = clamp(posX * ratio, 0, maxX());
targetX = clamp(targetX * ratio, 0, maxX());
el.style.left = `${posX}px`;
}
lastTrackWidth = newWidth;
});
setTimeout(() => { targetX = randomTarget(); }, 800 + Math.random() * 1500);
window.requestAnimationFrame(loop);
})(); })();
+2
View File
@@ -26,6 +26,8 @@ function isActive(href: string) {
~/hadi ~/hadi
</a> </a>
<div id="oneko-track" class="flex-1 relative h-12 pointer-events-none"></div>
<nav class="hidden md:flex items-center"> <nav class="hidden md:flex items-center">
{ {
links.map((link) => ( links.map((link) => (