this repo has no description
at main 7.1 kB view raw
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})();