From ae2ce608935993583304a0e1e6a301bf69ccc75b Mon Sep 17 00:00:00 2001
From: Ophestra
Date: Sun, 29 Jun 2025 02:52:42 +0900
Subject: [PATCH] static: replace error page
Also add cat to error page.
---
static/404.html | 15 ++---
static/js/oneko.js | 165 +++++++++++++++++++++++++++++++++++++++++++++
static/oneko.gif | Bin 0 -> 3316 bytes
3 files changed, 172 insertions(+), 8 deletions(-)
create mode 100644 static/js/oneko.js
create mode 100644 static/oneko.gif
diff --git a/static/404.html b/static/404.html
index c9cbca36..f3de5a77 100644
--- a/static/404.html
+++ b/static/404.html
@@ -2,22 +2,20 @@
- Page not found | GrapheneOS
+ Page not found | Hakurei
-
-
-
+
-
-
+
+
@@ -25,7 +23,8 @@
[[css|/main.css]]
-
+
+ [[js|/js/oneko.js]]
{% include "header.html" %}
@@ -33,7 +32,7 @@
The requested page does not exist. If you think this is a mistake, please
- report an issue.
+ report an issue.
{% include "footer.html" %}
diff --git a/static/js/oneko.js b/static/js/oneko.js
new file mode 100644
index 00000000..2f01bbcf
--- /dev/null
+++ b/static/js/oneko.js
@@ -0,0 +1,165 @@
+(function oneko() {
+ const nekoEl = document.createElement("div");
+ 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]],
+ scratch: [
+ [-5, 0],
+ [-6, 0],
+ [-7, 0],
+ ],
+ 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 create() {
+ nekoEl.id = "oneko";
+ nekoEl.style.width = "32px";
+ nekoEl.style.height = "32px";
+ nekoEl.style.position = "fixed";
+ nekoEl.style.pointerEvents = "none";
+ nekoEl.style.backgroundImage = "url('/oneko.gif')";
+ nekoEl.style.imageRendering = "pixelated";
+ nekoEl.style.left = "16px";
+ nekoEl.style.top = "16px";
+
+ document.body.appendChild(nekoEl);
+
+ document.onmousemove = (event) => {
+ mousePosX = event.clientX;
+ mousePosY = event.clientY;
+ };
+
+ window.onekoInterval = setInterval(frame, 100);
+ }
+
+ function setSprite(name, frame) {
+ const sprite = spriteSets[name][frame % spriteSets[name].length];
+ nekoEl.style.backgroundPosition = `${sprite[0] * 32}px ${
+ sprite[1] * 32
+ }px`;
+ }
+
+ function resetIdleAnimation() {
+ idleAnimation = null;
+ idleAnimationFrame = 0;
+ }
+
+ function idle() {
+ idleTime += 1;
+
+ // every ~ 20 seconds
+ if (
+ idleTime > 10 &&
+ Math.floor(Math.random() * 200) == 0 &&
+ idleAnimation == null
+ ) {
+ idleAnimation = ["sleeping", "scratch"][
+ Math.floor(Math.random() * 2)
+ ];
+ }
+
+ switch (idleAnimation) {
+ case "sleeping":
+ if (idleAnimationFrame < 8) {
+ setSprite("tired", 0);
+ break;
+ }
+ setSprite("sleeping", Math.floor(idleAnimationFrame / 4));
+ if (idleAnimationFrame > 192) {
+ resetIdleAnimation();
+ }
+ break;
+ case "scratch":
+ setSprite("scratch", idleAnimationFrame);
+ if (idleAnimationFrame > 9) {
+ resetIdleAnimation();
+ }
+ break;
+ default:
+ setSprite("idle", 0);
+ return;
+ }
+ idleAnimationFrame += 1;
+ }
+
+ function frame() {
+ frameCount += 1;
+ const diffX = nekoPosX - mousePosX;
+ const diffY = nekoPosY - mousePosY;
+ const distance = Math.sqrt(diffX ** 2 + diffY ** 2);
+
+ if (distance < nekoSpeed || distance < 48) {
+ idle();
+ return;
+ }
+
+ idleAnimation = null;
+ idleAnimationFrame = 0;
+
+ if (idleTime > 1) {
+ setSprite("alert", 0);
+ // count down after being alerted before moving
+ idleTime = Math.min(idleTime, 7);
+ idleTime -= 1;
+ return;
+ }
+
+ let 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;
+
+ nekoEl.style.left = `${nekoPosX - 16}px`;
+ nekoEl.style.top = `${nekoPosY - 16}px`;
+ }
+
+ create();
+})();
diff --git a/static/oneko.gif b/static/oneko.gif
new file mode 100644
index 0000000000000000000000000000000000000000..a009c2cc19c96b001ac76e96f27e5fa1a9e56577
GIT binary patch
literal 3316
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
F1^`&`O8o!;
literal 0
HcmV?d00001