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