Files
blog/public/scripts/oneko.js
2026-04-25 15:45:44 +02:00

185 lines
4.0 KiB
JavaScript

// oneko.js — navbar wanderer (based on https://github.com/adryd325/oneko.js)
(function oneko() {
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
const SIZE = 32;
const SPEED = 10;
const spriteSets = {
idle: [[-3, -3]],
alert: [[-7, -3]],
scratchSelf: [
[-5, 0],
[-6, 0],
[-7, 0],
],
scratchWallE: [
[-2, -2],
[-2, -3],
],
scratchWallW: [
[-4, 0],
[-4, -1],
],
tired: [[-3, -2]],
sleeping: [
[-2, 0],
[-2, -1],
],
E: [
[-3, 0],
[-3, -1],
],
W: [
[-4, -2],
[-4, -3],
],
};
const track = document.getElementById("oneko-track");
if (!track) return;
const el = document.createElement("div");
el.id = "oneko";
el.ariaHidden = "true";
el.style.width = `${SIZE}px`;
el.style.height = `${SIZE}px`;
el.style.position = "absolute";
el.style.bottom = "0";
el.style.pointerEvents = "none";
el.style.imageRendering = "pixelated";
el.style.zIndex = "2147483647";
el.style.backgroundImage = "url(/oneko.gif)";
track.appendChild(el);
function maxX() {
return track.offsetWidth - SIZE;
}
function clamp(v, lo, hi) {
return Math.max(lo, Math.min(hi, v));
}
function randomTarget() {
return Math.random() * maxX();
}
function setSprite(name, frame) {
const s = spriteSets[name][frame % spriteSets[name].length];
el.style.backgroundPosition = `${s[0] * SIZE}px ${s[1] * SIZE}px`;
}
let posX = randomTarget();
let targetX = posX;
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() {
idleTime++;
if (idleTime > 40 && Math.floor(Math.random() * 120) === 0) {
targetX = randomTarget();
idleTime = 0;
resetIdle();
return;
}
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":
if (idleAnimFrame < 8) {
setSprite("tired", 0);
break;
}
setSprite("sleeping", Math.floor(idleAnimFrame / 4));
if (idleAnimFrame > 192) resetIdle();
break;
case "scratchWallE":
case "scratchWallW":
case "scratchSelf":
setSprite(idleAnim, idleAnimFrame);
if (idleAnimFrame > 9) resetIdle();
break;
default:
setSprite("idle", 0);
return;
}
idleAnimFrame++;
}
function frame() {
frameCount++;
const diff = targetX - posX;
const dist = Math.abs(diff);
if (dist < SPEED) {
posX = targetX;
idle();
return;
}
resetIdle();
if (idleTime > 5) {
setSprite("alert", 0);
idleTime = Math.min(idleTime, 7) - 1;
return;
}
idleTime = 0;
setSprite(diff > 0 ? "E" : "W", frameCount);
posX = clamp(posX + (diff > 0 ? SPEED : -SPEED), 0, maxX());
el.style.left = `${posX}px`;
}
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);
})();