ONEKO FTW!!!!!!

This commit is contained in:
2025-12-17 20:10:51 +08:00
parent 8410fd673b
commit a7da171b21
3 changed files with 200 additions and 7 deletions

BIN
src/assets/oneko/oneko.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -5,6 +5,8 @@
} from "../../events/cursor"; } from "../../events/cursor";
import { appData } from "../../events/app-data"; import { appData } from "../../events/app-data";
import DesktopPet from "./DesktopPet.svelte";
function getFriendName(userId: string) { function getFriendName(userId: string) {
const friend = $appData?.friends?.find((f) => f.friend.id === userId); const friend = $appData?.friends?.find((f) => f.friend.id === userId);
return friend ? friend.friend.name : userId.slice(0, 8) + "..."; return friend ? friend.friend.name : userId.slice(0, 8) + "...";
@@ -56,13 +58,11 @@
<div class="absolute inset-0 size-full"> <div class="absolute inset-0 size-full">
{#if Object.keys($friendsCursorPositions).length > 0} {#if Object.keys($friendsCursorPositions).length > 0}
{#each Object.entries($friendsCursorPositions) as [userId, position]} {#each Object.entries($friendsCursorPositions) as [userId, position]}
<div <DesktopPet
style:transform="translate({position.raw.x}px, {position.raw.y}px)" targetX={position.raw.x}
class="transition-transform duration-500 flex flex-col gap-1" targetY={position.raw.y}
> name={getFriendName(userId)}
<div class="size-4 rounded-full bg-indigo-500"></div> />
<span class="text-[0.5rem]">{getFriendName(userId)}</span>
</div>
{/each} {/each}
{/if} {/if}
</div> </div>

View File

@@ -0,0 +1,193 @@
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import onekoGif from "../../assets/oneko/oneko.gif";
export let targetX = 0;
export let targetY = 0;
export let name = "";
let nekoPosX = 32;
let nekoPosY = 32;
let frameCount = 0;
let idleTime = 0;
let currentSprite = { x: -3, y: -3 }; // idle sprite initially
const nekoSpeed = 10;
let animationFrameId: number;
let lastFrameTimestamp: number;
// Sprite constants from oneko.js
const spriteSets: Record<string, [number, 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],
],
};
function setSprite(name: string, frame: number) {
const sprites = spriteSets[name];
const sprite = sprites[frame % sprites.length];
currentSprite = { x: sprite[0] * 32, y: sprite[1] * 32 };
}
function frame(timestamp: number) {
if (!lastFrameTimestamp) {
lastFrameTimestamp = timestamp;
}
// 100ms per frame for the animation loop
if (timestamp - lastFrameTimestamp > 100) {
lastFrameTimestamp = timestamp;
updatePosition();
}
animationFrameId = requestAnimationFrame(frame);
}
function updatePosition() {
frameCount += 1;
const diffX = nekoPosX - targetX;
const diffY = nekoPosY - targetY;
const distance = Math.sqrt(diffX ** 2 + diffY ** 2);
// If close enough, stop moving and idle
if (distance < nekoSpeed || distance < 48) {
setSprite("idle", 0);
idleTime += 1;
return;
}
// Alert behavior: pause briefly before moving if we were idling
if (idleTime > 1) {
setSprite("alert", 0);
idleTime = Math.min(idleTime, 7);
idleTime -= 1;
return;
}
idleTime = 0;
// Calculate direction
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" : "";
// Fallback if direction is empty (shouldn't happen with logic above but good safety)
if (direction === "") direction = "idle";
setSprite(direction, frameCount);
// Move towards target
nekoPosX -= (diffX / distance) * nekoSpeed;
nekoPosY -= (diffY / distance) * nekoSpeed;
}
onMount(() => {
// Initialize position to target so it doesn't fly in from 32,32 every time
nekoPosX = targetX;
nekoPosY = targetY;
animationFrameId = requestAnimationFrame(frame);
});
onDestroy(() => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
});
</script>
<div
class="desktop-pet flex flex-col items-center"
style="
transform: translate({nekoPosX - 16}px, {nekoPosY - 16}px);
z-index: 50;
"
>
<div
class="pixelated"
style="
width: 32px;
height: 32px;
background-image: url({onekoGif});
background-position: {currentSprite.x}px {currentSprite.y}px;
"
></div>
<span
class="text-[10px] bg-black/50 text-white px-1 rounded backdrop-blur-sm mt-1 whitespace-nowrap"
>
{name}
</span>
</div>
<style>
.desktop-pet {
position: fixed; /* Fixed relative to the viewport/container */
top: 0;
left: 0;
pointer-events: none; /* Let clicks pass through */
will-change: transform;
}
.pixelated {
image-rendering: pixelated;
}
</style>