this repo has no description
1// oneko.js: https://github.com/adryd325/oneko.js
2// petable version from https://github.com/tylxr59/oneko.js
3
4(function oneko() {
5 const isReducedMotion =
6 window.matchMedia(`(prefers-reduced-motion: reduce)`) === true ||
7 window.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true;
8
9 if (isReducedMotion) return;
10
11 const nekoEl = document.createElement("div");
12
13 let nekoPosX = 32;
14 let nekoPosY = 32;
15
16 let mousePosX = 0;
17 let mousePosY = 0;
18
19 let frameCount = 0;
20 let idleTime = 0;
21 let idleAnimation = null;
22 let idleAnimationFrame = 0;
23
24 // up neko speed from default 10
25 const nekoSpeed = 15;
26 const spriteSets = {
27 idle: [[-3, -3]],
28 alert: [[-7, -3]],
29 scratchSelf: [
30 [-5, 0],
31 [-6, 0],
32 [-7, 0],
33 ],
34 scratchWallN: [
35 [0, 0],
36 [0, -1],
37 ],
38 scratchWallS: [
39 [-7, -1],
40 [-6, -2],
41 ],
42 scratchWallE: [
43 [-2, -2],
44 [-2, -3],
45 ],
46 scratchWallW: [
47 [-4, 0],
48 [-4, -1],
49 ],
50 tired: [[-3, -2]],
51 sleeping: [
52 [-2, 0],
53 [-2, -1],
54 ],
55 N: [
56 [-1, -2],
57 [-1, -3],
58 ],
59 NE: [
60 [0, -2],
61 [0, -3],
62 ],
63 E: [
64 [-3, 0],
65 [-3, -1],
66 ],
67 SE: [
68 [-5, -1],
69 [-5, -2],
70 ],
71 S: [
72 [-6, -3],
73 [-7, -2],
74 ],
75 SW: [
76 [-5, -3],
77 [-6, -1],
78 ],
79 W: [
80 [-4, -2],
81 [-4, -3],
82 ],
83 NW: [
84 [-1, 0],
85 [-1, -1],
86 ],
87 };
88
89 function init() {
90 nekoEl.id = "oneko";
91 nekoEl.ariaHidden = true;
92 nekoEl.style.width = "32px";
93 nekoEl.style.height = "32px";
94 nekoEl.style.position = "fixed";
95 nekoEl.style.pointerEvents = "auto";
96 nekoEl.style.imageRendering = "pixelated";
97 nekoEl.style.left = `${nekoPosX - 16}px`;
98 nekoEl.style.top = `${nekoPosY - 16}px`;
99 nekoEl.style.zIndex = Number.MAX_VALUE;
100
101 // Personal edit: Fetch gif from root not relative path
102 let nekoFile = "/oneko.gif"
103 const curScript = document.currentScript
104 if (curScript && curScript.dataset.cat) {
105 nekoFile = curScript.dataset.cat
106 }
107 nekoEl.style.backgroundImage = `url(${nekoFile})`;
108
109 document.body.appendChild(nekoEl);
110
111 document.addEventListener("mousemove", function (event) {
112 mousePosX = event.clientX;
113 mousePosY = event.clientY;
114 });
115
116 window.requestAnimationFrame(onAnimationFrame);
117 }
118
119 let lastFrameTimestamp;
120
121 function onAnimationFrame(timestamp) {
122 // Stops execution if the neko element is removed from DOM
123 if (!nekoEl.isConnected) {
124 return;
125 }
126 if (!lastFrameTimestamp) {
127 lastFrameTimestamp = timestamp;
128 }
129 if (timestamp - lastFrameTimestamp > 100) {
130 lastFrameTimestamp = timestamp
131 frame()
132 }
133 window.requestAnimationFrame(onAnimationFrame);
134 }
135
136 function setSprite(name, frame) {
137 const sprite = spriteSets[name][frame % spriteSets[name].length];
138 nekoEl.style.backgroundPosition = `${sprite[0] * 32}px ${sprite[1] * 32}px`;
139 }
140
141 function resetIdleAnimation() {
142 idleAnimation = null;
143 idleAnimationFrame = 0;
144 }
145
146 function idle() {
147 idleTime += 1;
148
149 // every ~ 20 seconds
150 if (
151 idleTime > 10 &&
152 Math.floor(Math.random() * 200) == 0 &&
153 idleAnimation == null
154 ) {
155 let avalibleIdleAnimations = ["sleeping", "scratchSelf"];
156 if (nekoPosX < 32) {
157 avalibleIdleAnimations.push("scratchWallW");
158 }
159 if (nekoPosY < 32) {
160 avalibleIdleAnimations.push("scratchWallN");
161 }
162 if (nekoPosX > window.innerWidth - 32) {
163 avalibleIdleAnimations.push("scratchWallE");
164 }
165 if (nekoPosY > window.innerHeight - 32) {
166 avalibleIdleAnimations.push("scratchWallS");
167 }
168 idleAnimation =
169 avalibleIdleAnimations[
170 Math.floor(Math.random() * avalibleIdleAnimations.length)
171 ];
172 }
173
174 switch (idleAnimation) {
175 case "sleeping":
176 if (idleAnimationFrame < 8) {
177 setSprite("tired", 0);
178 break;
179 }
180 setSprite("sleeping", Math.floor(idleAnimationFrame / 4));
181 if (idleAnimationFrame > 192) {
182 resetIdleAnimation();
183 }
184 break;
185 case "scratchWallN":
186 case "scratchWallS":
187 case "scratchWallE":
188 case "scratchWallW":
189 case "scratchSelf":
190 setSprite(idleAnimation, idleAnimationFrame);
191 if (idleAnimationFrame > 9) {
192 resetIdleAnimation();
193 }
194 break;
195 default:
196 setSprite("idle", 0);
197 return;
198 }
199 idleAnimationFrame += 1;
200 }
201
202 function explodeHearts() {
203 const parent = nekoEl.parentElement;
204 const rect = nekoEl.getBoundingClientRect();
205 const scrollLeft = window.scrollX || document.documentElement.scrollLeft;
206 const scrollTop = window.scrollY || document.documentElement.scrollTop;
207 const centerX = rect.left + rect.width / 2 + scrollLeft;
208 const centerY = rect.top + rect.height / 2 + scrollTop;
209
210 for (let i = 0; i < 10; i++) {
211 const heart = document.createElement('div');
212 heart.className = 'heart';
213 heart.textContent = '❤';
214 const offsetX = (Math.random() - 0.5) * 50;
215 const offsetY = (Math.random() - 0.5) * 50;
216 heart.style.left = `${centerX + offsetX - 16}px`;
217 heart.style.top = `${centerY + offsetY - 16}px`;
218 heart.style.transform = `translate(-50%, -50%) rotate(${Math.random() * 360}deg)`;
219 parent.appendChild(heart);
220
221 setTimeout(() => {
222 parent.removeChild(heart);
223 }, 1000);
224 }
225 }
226
227 const style = document.createElement('style');
228 style.innerHTML = `
229 @keyframes heartBurst {
230 0% { transform: scale(0); opacity: 1; }
231 100% { transform: scale(1); opacity: 0; }
232 }
233 .heart {
234 position: absolute;
235 font-size: 2em;
236 animation: heartBurst 1s ease-out;
237 animation-fill-mode: forwards;
238 color: #ab9df2;
239 }
240 `;
241
242 document.head.appendChild(style);
243 nekoEl.addEventListener('click', explodeHearts);
244
245 function frame() {
246 frameCount += 1;
247 const diffX = nekoPosX - mousePosX;
248 const diffY = nekoPosY - mousePosY;
249 const distance = Math.sqrt(diffX ** 2 + diffY ** 2);
250
251 if (distance < nekoSpeed || distance < 48) {
252 idle();
253 return;
254 }
255
256 idleAnimation = null;
257 idleAnimationFrame = 0;
258
259 if (idleTime > 1) {
260 setSprite("alert", 0);
261 // count down after being alerted before moving
262 idleTime = Math.min(idleTime, 7);
263 idleTime -= 1;
264 return;
265 }
266
267 let direction;
268 direction = diffY / distance > 0.5 ? "N" : "";
269 direction += diffY / distance < -0.5 ? "S" : "";
270 direction += diffX / distance > 0.5 ? "W" : "";
271 direction += diffX / distance < -0.5 ? "E" : "";
272 setSprite(direction, frameCount);
273
274 nekoPosX -= (diffX / distance) * nekoSpeed;
275 nekoPosY -= (diffY / distance) * nekoSpeed;
276
277 nekoPosX = Math.min(Math.max(16, nekoPosX), window.innerWidth - 16);
278 nekoPosY = Math.min(Math.max(16, nekoPosY), window.innerHeight - 16);
279
280 nekoEl.style.left = `${nekoPosX - 16}px`;
281 nekoEl.style.top = `${nekoPosY - 16}px`;
282 }
283
284 init();
285})();