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 let nekoFile = "./oneko.gif" 102 const curScript = document.currentScript 103 if (curScript && curScript.dataset.cat) { 104 nekoFile = curScript.dataset.cat 105 } 106 nekoEl.style.backgroundImage = `url(${nekoFile})`; 107 108 document.body.appendChild(nekoEl); 109 110 document.addEventListener("mousemove", function (event) { 111 mousePosX = event.clientX; 112 mousePosY = event.clientY; 113 }); 114 115 window.requestAnimationFrame(onAnimationFrame); 116 } 117 118 let lastFrameTimestamp; 119 120 function onAnimationFrame(timestamp) { 121 // Stops execution if the neko element is removed from DOM 122 if (!nekoEl.isConnected) { 123 return; 124 } 125 if (!lastFrameTimestamp) { 126 lastFrameTimestamp = timestamp; 127 } 128 if (timestamp - lastFrameTimestamp > 100) { 129 lastFrameTimestamp = timestamp 130 frame() 131 } 132 window.requestAnimationFrame(onAnimationFrame); 133 } 134 135 function setSprite(name, frame) { 136 const sprite = spriteSets[name][frame % spriteSets[name].length]; 137 nekoEl.style.backgroundPosition = `${sprite[0] * 32}px ${sprite[1] * 32}px`; 138 } 139 140 function resetIdleAnimation() { 141 idleAnimation = null; 142 idleAnimationFrame = 0; 143 } 144 145 function idle() { 146 idleTime += 1; 147 148 // every ~ 20 seconds 149 if ( 150 idleTime > 10 && 151 Math.floor(Math.random() * 200) == 0 && 152 idleAnimation == null 153 ) { 154 let avalibleIdleAnimations = ["sleeping", "scratchSelf"]; 155 if (nekoPosX < 32) { 156 avalibleIdleAnimations.push("scratchWallW"); 157 } 158 if (nekoPosY < 32) { 159 avalibleIdleAnimations.push("scratchWallN"); 160 } 161 if (nekoPosX > window.innerWidth - 32) { 162 avalibleIdleAnimations.push("scratchWallE"); 163 } 164 if (nekoPosY > window.innerHeight - 32) { 165 avalibleIdleAnimations.push("scratchWallS"); 166 } 167 idleAnimation = 168 avalibleIdleAnimations[ 169 Math.floor(Math.random() * avalibleIdleAnimations.length) 170 ]; 171 } 172 173 switch (idleAnimation) { 174 case "sleeping": 175 if (idleAnimationFrame < 8) { 176 setSprite("tired", 0); 177 break; 178 } 179 setSprite("sleeping", Math.floor(idleAnimationFrame / 4)); 180 if (idleAnimationFrame > 192) { 181 resetIdleAnimation(); 182 } 183 break; 184 case "scratchWallN": 185 case "scratchWallS": 186 case "scratchWallE": 187 case "scratchWallW": 188 case "scratchSelf": 189 setSprite(idleAnimation, idleAnimationFrame); 190 if (idleAnimationFrame > 9) { 191 resetIdleAnimation(); 192 } 193 break; 194 default: 195 setSprite("idle", 0); 196 return; 197 } 198 idleAnimationFrame += 1; 199 } 200 201 function explodeHearts() { 202 const parent = nekoEl.parentElement; 203 const rect = nekoEl.getBoundingClientRect(); 204 const scrollLeft = window.scrollX || document.documentElement.scrollLeft; 205 const scrollTop = window.scrollY || document.documentElement.scrollTop; 206 const centerX = rect.left + rect.width / 2 + scrollLeft; 207 const centerY = rect.top + rect.height / 2 + scrollTop; 208 209 for (let i = 0; i < 10; i++) { 210 const heart = document.createElement('div'); 211 heart.className = 'heart'; 212 heart.textContent = '❤'; 213 const offsetX = (Math.random() - 0.5) * 50; 214 const offsetY = (Math.random() - 0.5) * 50; 215 heart.style.left = `${centerX + offsetX - 16}px`; 216 heart.style.top = `${centerY + offsetY - 16}px`; 217 heart.style.transform = `translate(-50%, -50%) rotate(${Math.random() * 360}deg)`; 218 parent.appendChild(heart); 219 220 setTimeout(() => { 221 parent.removeChild(heart); 222 }, 1000); 223 } 224 } 225 226 const style = document.createElement('style'); 227 style.innerHTML = ` 228 @keyframes heartBurst { 229 0% { transform: scale(0); opacity: 1; } 230 100% { transform: scale(1); opacity: 0; } 231 } 232 .heart { 233 position: absolute; 234 font-size: 2em; 235 animation: heartBurst 1s ease-out; 236 animation-fill-mode: forwards; 237 color: #ab9df2; 238 } 239 `; 240 241 document.head.appendChild(style); 242 nekoEl.addEventListener('click', explodeHearts); 243 244 function frame() { 245 frameCount += 1; 246 const diffX = nekoPosX - mousePosX; 247 const diffY = nekoPosY - mousePosY; 248 const distance = Math.sqrt(diffX ** 2 + diffY ** 2); 249 250 if (distance < nekoSpeed || distance < 48) { 251 idle(); 252 return; 253 } 254 255 idleAnimation = null; 256 idleAnimationFrame = 0; 257 258 if (idleTime > 1) { 259 setSprite("alert", 0); 260 // count down after being alerted before moving 261 idleTime = Math.min(idleTime, 7); 262 idleTime -= 1; 263 return; 264 } 265 266 let direction; 267 direction = diffY / distance > 0.5 ? "N" : ""; 268 direction += diffY / distance < -0.5 ? "S" : ""; 269 direction += diffX / distance > 0.5 ? "W" : ""; 270 direction += diffX / distance < -0.5 ? "E" : ""; 271 setSprite(direction, frameCount); 272 273 nekoPosX -= (diffX / distance) * nekoSpeed; 274 nekoPosY -= (diffY / distance) * nekoSpeed; 275 276 nekoPosX = Math.min(Math.max(16, nekoPosX), window.innerWidth - 16); 277 nekoPosY = Math.min(Math.max(16, nekoPosY), window.innerHeight - 16); 278 279 nekoEl.style.left = `${nekoPosX - 16}px`; 280 nekoEl.style.top = `${nekoPosY - 16}px`; 281 } 282 283 init(); 284})();