diff --git a/website/assets/oneko.webp b/website/assets/oneko.webp
new file mode 100644
index 0000000..7724bd4
Binary files /dev/null and b/website/assets/oneko.webp differ
diff --git a/website/assets/style.css b/website/assets/style.css
index 4dde415..ce8aa04 100644
--- a/website/assets/style.css
+++ b/website/assets/style.css
@@ -115,4 +115,7 @@ h6 {
.giscus-outer {
max-height:unset;
}
+ #oneko {
+ display: none;
+ }
}
\ No newline at end of file
diff --git a/website/index.html b/website/index.html
index 9976412..e7132ba 100644
--- a/website/index.html
+++ b/website/index.html
@@ -22,9 +22,10 @@ __TEMPLATE_HEAD__
☹️☹️☹️.ovh
I'm Latvian, 17. My name's Sophie meows a lot. I love listening to music.
-
+
I love to play games with peeps! My favorite games recently have been Minecraft and Stardew Valley! DM me
- if you wanna play with me ^w^
+ if you wanna play with me ^w^
+
DNI:
diff --git a/website/scripts/oneko.ts b/website/scripts/oneko.ts
new file mode 100644
index 0000000..3883950
--- /dev/null
+++ b/website/scripts/oneko.ts
@@ -0,0 +1,243 @@
+class Neko {
+ element: HTMLDivElement;
+
+ readonly nekoSpeed = 10;
+ readonly spriteSheets: Record = {
+ 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],
+ ],
+ };
+ private nekoPosX = 32;
+ private nekoPosY = 32;
+
+ private mousePosX = 0;
+ private mousePosY = 0;
+
+ private frameCount = 0;
+ private idleTime = 0;
+ private idleAnimation?: string;
+ private idleAnimationFrame = 0;
+ private lastFrameTimestamp?: number;
+ private file: string;
+
+ constructor(file: string = "assets/oneko.webp") {
+ this.file = file;
+
+ this.element = document.createElement("div");
+
+ this.element.id = "oneko";
+ this.element.ariaHidden = "true";
+ this.element.style.width = "32px";
+ this.element.style.height = "32px";
+ this.element.style.position = "fixed";
+ this.element.style.pointerEvents = "none";
+ this.element.style.imageRendering = "pixelated";
+ this.element.style.left = `${this.nekoPosX - 16}px`;
+ this.element.style.top = `${this.nekoPosY - 16}px`;
+ this.element.style.zIndex = "2147483647";
+
+ this.element.style.backgroundImage = `url(${this.file})`;
+ let element = document.body;
+
+ if (!element) {
+ throw new Error("no body exists");
+ }
+
+ element.appendChild(this.element);
+ document.addEventListener("mousemove", (event) => {
+ this.mousePosX = event.clientX;
+ this.mousePosY = event.clientY;
+ });
+
+ window.requestAnimationFrame((t) => this.onAnimationFrame(t));
+ }
+ private onAnimationFrame(timestamp: number) {
+ // Stops execution if the neko element is removed from DOM
+ if (!this.element.isConnected) {
+ return;
+ }
+ if (!this.lastFrameTimestamp) {
+ this.lastFrameTimestamp = timestamp;
+ }
+ if (timestamp - this.lastFrameTimestamp > 100) {
+ this.lastFrameTimestamp = timestamp;
+ this.frame();
+ }
+ window.requestAnimationFrame((t) => this.onAnimationFrame(t));
+ }
+
+ setSprite(name: string, frame: number) {
+ const sprite =
+ this.spriteSheets[name][frame % this.spriteSheets[name].length];
+ this.element.style.backgroundPosition = `${sprite[0] * 32}px ${
+ sprite[1] * 32
+ }px`;
+ }
+
+ resetidleAnimation() {
+ this.idleAnimation = undefined;
+ this.idleAnimationFrame = 0;
+ }
+
+ idle() {
+ this.idleTime += 1;
+
+ // every ~ 20 seconds
+ if (
+ this.idleTime > 10 &&
+ Math.floor(Math.random() * 200) == 0 &&
+ this.idleAnimation == null
+ ) {
+ let availableidleAnimations = ["sleeping", "scratchSelf"];
+ if (this.nekoPosX < 32) {
+ availableidleAnimations.push("scratchWallW");
+ }
+ if (this.nekoPosY < 32) {
+ availableidleAnimations.push("scratchWallN");
+ }
+ if (this.nekoPosX > window.innerWidth - 32) {
+ availableidleAnimations.push("scratchWallE");
+ }
+ if (this.nekoPosY > window.innerHeight - 32) {
+ availableidleAnimations.push("scratchWallS");
+ }
+ this.idleAnimation =
+ availableidleAnimations[
+ Math.floor(Math.random() * availableidleAnimations.length)
+ ];
+ }
+
+ switch (this.idleAnimation) {
+ case "sleeping":
+ if (this.idleAnimationFrame < 8) {
+ this.setSprite("tired", 0);
+ break;
+ }
+ this.setSprite("sleeping", Math.floor(this.idleAnimationFrame / 4));
+ if (this.idleAnimationFrame > 192) {
+ this.resetidleAnimation();
+ }
+ break;
+ case "scratchWallN":
+ case "scratchWallS":
+ case "scratchWallE":
+ case "scratchWallW":
+ case "scratchSelf":
+ this.setSprite(this.idleAnimation, this.idleAnimationFrame);
+ if (this.idleAnimationFrame > 9) {
+ this.resetidleAnimation();
+ }
+ break;
+ default:
+ this.setSprite("idle", 0);
+ return;
+ }
+ this.idleAnimationFrame += 1;
+ }
+
+ frame() {
+ this.frameCount += 1;
+ const diffX = this.nekoPosX - this.mousePosX;
+ const diffY = this.nekoPosY - this.mousePosY;
+ const distance = Math.sqrt(diffX ** 2 + diffY ** 2);
+
+ if (distance < this.nekoSpeed || distance < 48) {
+ this.idle();
+ return;
+ }
+
+ this.idleAnimation = undefined;
+ this.idleAnimationFrame = 0;
+
+ if (this.idleTime > 1) {
+ this.setSprite("alert", 0);
+ // count down after being alerted before moving
+ this.idleTime = Math.min(this.idleTime, 7);
+ this.idleTime -= 1;
+ return;
+ }
+
+ let direction: string;
+ direction = diffY / distance > 0.5 ? "N" : "";
+ direction += diffY / distance < -0.5 ? "S" : "";
+ direction += diffX / distance > 0.5 ? "W" : "";
+ direction += diffX / distance < -0.5 ? "E" : "";
+ this.setSprite(direction, this.frameCount);
+
+ this.nekoPosX -= (diffX / distance) * this.nekoSpeed;
+ this.nekoPosY -= (diffY / distance) * this.nekoSpeed;
+
+ this.nekoPosX = Math.min(
+ Math.max(16, this.nekoPosX),
+ window.innerWidth - 16
+ );
+ this.nekoPosY = Math.min(
+ Math.max(16, this.nekoPosY),
+ window.innerHeight - 16
+ );
+
+ this.element.style.left = `${this.nekoPosX - 16}px`;
+ this.element.style.top = `${this.nekoPosY - 16}px`;
+ }
+}
+
+window.addEventListener("load", () => {
+ new Neko("/assets/oneko.webp");
+});
diff --git a/website/templates/head.html b/website/templates/head.html
index 71b262d..6c57871 100644
--- a/website/templates/head.html
+++ b/website/templates/head.html
@@ -23,4 +23,4 @@
-
\ No newline at end of file
+
\ No newline at end of file