diff --git a/public/scripts/oneko.js b/public/scripts/oneko.js index 83efa67..648b17b 100644 --- a/public/scripts/oneko.js +++ b/public/scripts/oneko.js @@ -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() { - const isReducedMotion = - window.matchMedia(`(prefers-reduced-motion: reduce)`) === true || - window.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true; + if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; - 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 = { - idle: [[-3, -3]], - alert: [[-7, -3]], - scratchSelf: [ - [-5, 0], - [-6, 0], - [-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]], - 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], - ], + 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]], }; - function init() { - let nekoFile = "/oneko.gif"; - 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(), - ); - } - } + const track = document.getElementById("oneko-track"); + if (!track) return; - if (persistPosition) { - let storedNeko = JSON.parse(window.localStorage.getItem("oneko")); - if (storedNeko !== null) { - nekoPosX = storedNeko.nekoPosX; - nekoPosY = storedNeko.nekoPosY; - mousePosX = storedNeko.mousePosX; - mousePosY = storedNeko.mousePosY; - frameCount = storedNeko.frameCount; - idleTime = storedNeko.idleTime; - idleAnimation = storedNeko.idleAnimation; - idleAnimationFrame = storedNeko.idleAnimationFrame; - nekoEl.style.backgroundPosition = storedNeko.bgPos; - } - } + 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); - nekoEl.id = "oneko"; - nekoEl.ariaHidden = true; - nekoEl.style.width = "32px"; - 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 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 sprite = spriteSets[name][frame % spriteSets[name].length]; - nekoEl.style.backgroundPosition = `${sprite[0] * 32}px ${sprite[1] * 32}px`; + const s = spriteSets[name][frame % spriteSets[name].length]; + el.style.backgroundPosition = `${s[0] * SIZE}px ${s[1] * SIZE}px`; } - function resetIdleAnimation() { - idleAnimation = null; - idleAnimationFrame = 0; - } + 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 += 1; + idleTime++; - // every ~ 20 seconds - if ( - idleTime > 10 && - Math.floor(Math.random() * 200) == 0 && - idleAnimation == null - ) { - 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) - ]; + if (idleTime > 40 && Math.floor(Math.random() * 120) === 0) { + targetX = randomTarget(); + idleTime = 0; + resetIdle(); + return; } - 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": - if (idleAnimationFrame < 8) { - setSprite("tired", 0); - break; - } - setSprite("sleeping", Math.floor(idleAnimationFrame / 4)); - if (idleAnimationFrame > 192) { - resetIdleAnimation(); - } + if (idleAnimFrame < 8) { setSprite("tired", 0); break; } + setSprite("sleeping", Math.floor(idleAnimFrame / 4)); + if (idleAnimFrame > 192) resetIdle(); break; - case "scratchWallN": - case "scratchWallS": case "scratchWallE": case "scratchWallW": case "scratchSelf": - setSprite(idleAnimation, idleAnimationFrame); - if (idleAnimationFrame > 9) { - resetIdleAnimation(); - } + setSprite(idleAnim, idleAnimFrame); + if (idleAnimFrame > 9) resetIdle(); break; default: setSprite("idle", 0); return; } - idleAnimationFrame += 1; + idleAnimFrame++; } function frame() { - frameCount += 1; - const diffX = nekoPosX - mousePosX; - const diffY = nekoPosY - mousePosY; - const distance = Math.sqrt(diffX ** 2 + diffY ** 2); + frameCount++; + const diff = targetX - posX; + const dist = Math.abs(diff); - if (distance < nekoSpeed || distance < 48) { + if (dist < SPEED) { + posX = targetX; idle(); return; } - idleAnimation = null; - idleAnimationFrame = 0; + resetIdle(); - if (idleTime > 1) { + if (idleTime > 5) { setSprite("alert", 0); - // count down after being alerted before moving - idleTime = Math.min(idleTime, 7); - idleTime -= 1; + idleTime = Math.min(idleTime, 7) - 1; return; } + idleTime = 0; - let direction; - direction = diffY / distance > 0.5 ? "N" : ""; - direction += diffY / distance < -0.5 ? "S" : ""; - 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`; + setSprite(diff > 0 ? "E" : "W", frameCount); + posX = clamp(posX + (diff > 0 ? SPEED : -SPEED), 0, maxX()); + el.style.left = `${posX}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); })(); diff --git a/src/components/Navbar.astro b/src/components/Navbar.astro index efd5ab7..6d237a2 100644 --- a/src/components/Navbar.astro +++ b/src/components/Navbar.astro @@ -26,6 +26,8 @@ function isActive(href: string) { ~/hadi +
+