mirror of
https://github.com/anotherhadi/blog.git
synced 2026-05-20 05:32:32 +02:00
Keep the cat in the navbar !
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
+94
-237
@@ -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],
|
|
||||||
],
|
|
||||||
scratchWallN: [
|
|
||||||
[0, 0],
|
|
||||||
[0, -1],
|
|
||||||
],
|
|
||||||
scratchWallS: [
|
|
||||||
[-7, -1],
|
|
||||||
[-6, -2],
|
|
||||||
],
|
|
||||||
scratchWallE: [
|
|
||||||
[-2, -2],
|
|
||||||
[-2, -3],
|
|
||||||
],
|
|
||||||
scratchWallW: [
|
|
||||||
[-4, 0],
|
|
||||||
[-4, -1],
|
|
||||||
],
|
|
||||||
tired: [[-3, -2]],
|
tired: [[-3, -2]],
|
||||||
sleeping: [
|
sleeping: [[-2, 0], [-2, -1]],
|
||||||
[-2, 0],
|
E: [[-3, 0], [-3, -1]],
|
||||||
[-2, -1],
|
W: [[-4, -2], [-4, -3]],
|
||||||
],
|
|
||||||
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));
|
||||||
|
if (idleAnimFrame > 192) resetIdle();
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
setSprite("sleeping", Math.floor(idleAnimationFrame / 4));
|
|
||||||
if (idleAnimationFrame > 192) {
|
|
||||||
resetIdleAnimation();
|
|
||||||
}
|
|
||||||
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);
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user