This commit is contained in:
parent
5eab5815e1
commit
260bb91fd6
BIN
website/assets/oneko.webp
Normal file
BIN
website/assets/oneko.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
|
@ -115,4 +115,7 @@ h6 {
|
||||||
.giscus-outer {
|
.giscus-outer {
|
||||||
max-height:unset;
|
max-height:unset;
|
||||||
}
|
}
|
||||||
|
#oneko {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -24,7 +24,8 @@ __TEMPLATE_HEAD__
|
||||||
<p>I'm Latvian, 17. My name's Sophie <small>meows a lot</small>. I love listening to music.</p>
|
<p>I'm Latvian, 17. My name's Sophie <small>meows a lot</small>. I love listening to music.</p>
|
||||||
|
|
||||||
<p>I love to play games with peeps! My favorite games recently have been Minecraft and Stardew Valley! DM me
|
<p>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^</p>
|
if you wanna play with me ^w^
|
||||||
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
DNI:
|
DNI:
|
||||||
|
|
243
website/scripts/oneko.ts
Normal file
243
website/scripts/oneko.ts
Normal file
|
@ -0,0 +1,243 @@
|
||||||
|
class Neko {
|
||||||
|
element: HTMLDivElement;
|
||||||
|
|
||||||
|
readonly nekoSpeed = 10;
|
||||||
|
readonly spriteSheets: Record<string, number[][]> = {
|
||||||
|
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");
|
||||||
|
});
|
|
@ -23,4 +23,4 @@
|
||||||
<meta property="twitter:description" content="yourfriend.lol v2, soph's blog and information"" />
|
<meta property="twitter:description" content="yourfriend.lol v2, soph's blog and information"" />
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<script src="scripts/oneko.js"></script>
|
Loading…
Reference in a new issue