1let FRAME_DIM = 16; 2let multiplier = 1; 3let clampBleed = 0; 4const isMobile = () => { 5 let check = false; 6 // prettier-ignore 7 (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera); 8 return check; 9}; 10 11if (!isMobile()) { 12 clampBleed = 0; 13 FRAME_DIM = 32; 14 multiplier = 1.5; 15} 16let spriteSheet = document.querySelector(`#spritesheet-${FRAME_DIM}`); 17 18// Normalize coordinate system to use CSS pixels. 19const flyCanvas = document.createElement("canvas"); 20flyCanvas.style = 21 "position: absolute; top: 0; left: 0; pointer-events: none; image-rendering: crisp-edges; image-rendering: pixelated;"; 22let bodyDimensions = document.body.parentElement.getBoundingClientRect(); 23flyCanvas.width = bodyDimensions.width; 24flyCanvas.height = bodyDimensions.height; 25 26document.body.appendChild(flyCanvas); 27 28let obstacles = []; 29 30function run() { 31 let mouseX = undefined; 32 let mouseY = undefined; 33 document.body.addEventListener("mousemove", (event) => { 34 mouseX = event.pageX; 35 mouseY = event.pageY; 36 }); 37 // document.body.addEventListener("click", (event) => { 38 // fly.position.x = event.pageX; 39 // fly.position.y = event.pageY; 40 // }); 41 42 const TOP = 0; 43 const RIGHT = 1; 44 const BOTTOM = 2; 45 const LEFT = 3; 46 const ctx = flyCanvas.getContext("2d"); 47 48 const extractFrame = (index = 0) => { 49 const canvas = document.createElement("canvas"); 50 canvas.style.imageRendering = "pixelated"; 51 canvas.width = FRAME_DIM; 52 canvas.height = FRAME_DIM; 53 const ctx = canvas.getContext("2d"); 54 ctx.drawImage( 55 spriteSheet, 56 index * FRAME_DIM, 57 0, 58 FRAME_DIM, 59 FRAME_DIM, 60 0, 61 0, 62 FRAME_DIM, 63 FRAME_DIM 64 ); 65 return canvas; 66 }; 67 68 // my name is andy and i love javascript 69 const frames = Array.from({ length: 17 }) 70 .map((_, i) => i) 71 .reduce((acc, _, i) => { 72 return acc.concat(extractFrame(i)); 73 }, []); 74 75 const animationStates = { 76 idle: 0, 77 idleCleanLegs: 1, 78 idleCleanWings: 2, 79 walk: 3, 80 fly: 4, 81 }; 82 83 const framesForAnimations = { 84 [animationStates.idle]: [10], 85 [animationStates.idleCleanWings]: [7, 8, 7, 8, 7, 8, 9, 10], 86 [animationStates.idleCleanLegs]: [4, 5, 4, 5, 4, 5, 6, 10], 87 [animationStates.walk]: [11, 12, 13, 14, 15, 16], 88 [animationStates.fly]: [0, 1], 89 }; 90 91 const fpsForAnimations = { 92 [animationStates.idle]: 10, 93 [animationStates.idleCleanLegs]: 10, 94 [animationStates.idleCleanWings]: 10, 95 [animationStates.walk]: 30, 96 [animationStates.fly]: 30, 97 }; 98 99 const lerp = (a, b, t) => { 100 return a + (b - a) * t; 101 }; 102 103 const distance = (a, b) => { 104 return Math.sqrt((b.x - a.x) ** 2 + (b.y - a.y) ** 2); 105 }; 106 107 const clamp = (v, a, b) => { 108 return Math.min(b, Math.max(v, a)); 109 }; 110 111 const normalize = (vector, magnitude) => { 112 const m = Math.sqrt(vector.x ** 2 + vector.y ** 2); 113 return { 114 x: (vector.x / m) * magnitude, 115 y: (vector.y / m) * magnitude, 116 }; 117 }; 118 119 const obstacleNodes = Array.from(document.querySelectorAll(".blockfly")); 120 const getObstacleRects = () => 121 obstacleNodes.map((element) => { 122 const rect = element.getBoundingClientRect(); 123 const domRect = new DOMRect( 124 rect.left, 125 rect.top + window.scrollY, 126 rect.width, 127 rect.height 128 ); 129 // make a new observer to enclose around domrect 130 return domRect; 131 }); 132 obstacles = getObstacleRects(); 133 const observer = new ResizeObserver((entries) => { 134 obstacles = getObstacleRects(); 135 }); 136 window.addEventListener("resize", () => { 137 // mobile fires resize when scrolling because the address bar smushes 138 obstacles = getObstacleRects(); 139 let currentHeight = bodyDimensions.height; 140 let currentWidth = bodyDimensions.width; 141 bodyDimensions = document.body.parentElement.getBoundingClientRect(); 142 flyCanvas.width = bodyDimensions.width; 143 flyCanvas.height = bodyDimensions.height; 144 if ( 145 currentWidth != bodyDimensions.width && 146 fly.animationState < animationStates.fly 147 ) { 148 fly.flee(); 149 } 150 let newHeight = bodyDimensions.height; 151 if (newHeight < currentHeight) { 152 // its possible to get forced into an obstacle; to avoid that, move the fly vertically by oldH - newH 153 fly.position.y = clamp( 154 fly.position.y - (currentHeight - newHeight),
155 0, 156 bodyDimensions.height - fly.height 157 ); 158 } 159 }); 160 obstacleNodes.forEach((node) => observer.observe(node)); 161 162 const intersectingObstacle = (x, y, w, h, excludeObject = undefined) => { 163 const obstacleRects = obstacles; 164 for (let i = 0; i < obstacleRects.length; i++) { 165 const rect = obstacleRects[i]; 166 if ( 167 excludeObject && 168 rect.left == excludeObject.left && 169 rect.top == excludeObject.top && 170 rect.right == excludeObject.right && 171 rect.bottom == excludeObject.bottom 172 ) { 173 continue; 174 } 175 if ( 176 x + w > rect.left && 177 x < rect.right && 178 y + h > rect.top && 179 y < rect.bottom 180 ) { 181 return rect; 182 } 183 } 184 return undefined; 185 }; 186 187 FLYING = 0; 188 WALKING = 1; 189 190 class Fly { 191 state = FLYING; 192 animationState = animationStates.fly; 193 moveChance = 0.2; 194 last = performance.now(); 195 frame = 0; 196 flipX = 0; 197 flipY = 0; 198 rot = 0; 199 velocity = { x: 6 * multiplier, y: 3 * multiplier }; 200 height = FRAME_DIM; 201 width = FRAME_DIM; 202 clampDirection = LEFT; 203 localVelocity = this.velocity; 204 objectImSittingOn = undefined; 205 walkingDirection = 1; 206 fleeOnScrollListener = null; 207 208 constructor() { 209 this.position = { 210 x: 10, 211 y: 10, 212 }; 213 this.boundFleeCB = this.flee.bind(this); 214 } 215 randomizedVelocity() { 216 // buzz around, like a fly 217 const magnitude = Math.sqrt( 218 this.localVelocity.x ** 2 + this.localVelocity.y ** 2 219 ); 220 // only go a certain distance from your angle 221 let angle = Math.atan2(this.localVelocity.y, this.localVelocity.x); 222 angle += Math.PI * 2 * (0.5 - Math.random()) * 0.2; 223 const vx = lerp(magnitude * Math.cos(angle), this.velocity.x, 0.4); 224 const vy = lerp(magnitude * Math.sin(angle), this.velocity.y, 0.4); 225 return { 226 x: vx, 227 y: vy, 228 }; 229 } 230 chooseRandomDestination() { 231 // pick a random place you want to go :) that is where you will go :) 232 const destination = { 233 x: Math.random() * bodyDimensions.width, 234 y: Math.random() * bodyDimensions.height, 235 }; 236 const angle = Math.atan2( 237 this.position.y - destination.y, 238 destination.x - this.position.x 239 ); 240 this.velocity = this.localVelocity = { 241 x: Math.cos(angle) * 5 * multiplier, 242 y: Math.sin(angle) * 5 * multiplier, 243 }; 244 } 245 setRotationForClampDirection(clampDirection) { 246 switch (clampDirection) { 247 case LEFT: 248 this.rot = 90; 249 break; 250 case RIGHT: 251 this.rot = -90; 252 break; 253 case TOP: 254 this.rot = 180; 255 break; 256 case BOTTOM: 257 this.rot = 0; 258 break; 259 } 260 } 261 switchToIdle() { 262 this.setRotationForClampDirection(this.clampDirection); 263 this.frame = 0; 264 this.animationState = animationStates.idle; 265 this.enterIdleState(animationStates.idle); 266 this.velocity = { x: 0, y: 0 }; 267 } 268 switchToFlying() { 269 this.flipX = 0; 270 this.flipY = 0; 271 this.rot = 0; 272 this.frame = 0; 273 this.objectImSittingOn = undefined; 274 this.animationState = animationStates.fly; 275 window.removeEventListener("scroll", this.boundFleeCB); 276 window.clearTimeout(this.idleTimeout); 277 } 278 flee() { 279 this.chooseRandomDestination(); 280 if (this.clampDirection == RIGHT) {
281 this.velocity.x = -Math.abs(this.velocity.x); 282 } else if (this.clampDirection == LEFT) { 283 this.velocity.x = Math.abs(this.velocity.x); 284 } else if (this.clampDirection == TOP) { 285 this.velocity.y = Math.abs(this.velocity.y); 286 } else if (this.clampDirection == BOTTOM) { 287 this.velocity.y = -Math.abs(this.velocity.y); 288 } 289 // also apply that to the local one a bit. it's funny if not all the way 290 this.localVelocity.x = lerp( 291 this.localVelocity.x, 292 this.velocity.x, 293 0.4 294 ); 295 this.localVelocity.y = lerp( 296 this.localVelocity.y, 297 this.velocity.y, 298 0.4 299 ); 300 this.flipX = this.localVelocity.x < 0; 301 this.switchToFlying(); 302 } 303 enterIdleState(state) { 304 const r = Math.random(); 305 if (r < 1 - this.moveChance) { 306 this.setIdleTimeout(animationStates.idle); 307 this.moveChance += 0.1; 308 } else if (r < 0.8) { 309 this.numberOfWalkingFrames = 310 10 + Math.floor(Math.random() * 40); 311 this.animationState = animationStates.walk; 312 } else if (r < 0.2) { 313 this.switchToFlying(); 314 } else { 315 this.moveChance = 0.2; 316 this.animationState = 317 Math.random() < 0.5 318 ? animationStates.idleCleanLegs 319 : animationStates.idleCleanWings; 320 } 321 } 322 setIdleTimeout(state) { 323 this.animationState = state; 324 this.idleTimeout = setTimeout(() => { 325 this.enterIdleState(state); 326 }, 1000); 327 } 328 clear(x, y, mirrorX, mirrorY) { 329 ctx.save(); 330 ctx.translate(FRAME_DIM * mirrorX, FRAME_DIM * mirrorY); 331 ctx.scale(mirrorX ? -1 : 1, mirrorY ? -1 : 1); 332 ctx.clearRect( 333 x * (mirrorX ? -1 : 1), 334 y * (mirrorY ? -1 : 1), 335 FRAME_DIM, 336 FRAME_DIM 337 ); 338 ctx.restore(); 339 } 340 draw(frame, x, y, mirrorX, mirrorY) { 341 x |= x; 342 y |= y; 343 ctx.save(); 344 // ctx.translate(FRAME_DIM * mirrorX, FRAME_DIM * mirrorY); 345 ctx.translate(x + this.width / 2, y + this.height / 2); 346 ctx.rotate((this.rot * Math.PI) / 180); 347 ctx.scale(mirrorX ? -1 : 1, mirrorY ? -1 : 1); 348 ctx.drawImage( 349 frame, 350 0, 351 0, 352 this.width, 353 this.height, 354 -this.width / 2, 355 -this.height / 2, 356 this.width, 357 this.height 358 ); 359 ctx.restore(); 360 } 361 clampToObject(direction) { 362 this.clampDirection = direction; 363 switch (direction) { 364 case LEFT: 365 this.rot = 90; 366 break; 367 case RIGHT: 368 this.rot = -90; 369 break; 370 case TOP: 371 this.rot = 180; 372 break; 373 case BOTTOM: 374 this.rot = 0; 375 break; 376 } 377 } 378 nextState(state) { 379 if ( 380 state === animationStates.idleCleanLegs || 381 state === animationStates.idleCleanWings 382 ) { 383 return animationStates.idle; 384 } 385 if (state === animationStates.fly) { 386 // keep on keeping on little fly 387 return animationStates.fly; 388 } 389 return state; 390 } 391 updateState(elapsed) { 392 let frameIndices = framesForAnimations[this.animationState]; 393 let fps = fpsForAnimations[this.animationState]; 394 let startAnimationState = this.animationState; 395 if (elapsed > 1000 / fps) { 396 this.last = this.last + elapsed; 397 const nAdvance = 1; //Math.floor(elapsed / (1000 / fps)); 398 this.frame = this.frame + nAdvance; 399 if (this.frame > frameIndices.length - 1) { 400 this.animationState = this.nextState(this.animationState); 401 this.frame = 0; 402 } 403 } 404 if ( 405 this.animationState !== startAnimationState && 406 this.animationState === animationStates.idle 407 ) { 408 this.switchToIdle(); 409 frameIndices = framesForAnimations[this.animationState]; 410 } 411 return frames[frameIndices[this.frame]]; 412 } 413 update(elapsed) { 414 let frame = this.updateState(elapsed); 415 this.clear( 416 this.position.x, 417 this.position.y, 418 this.flipX, 419 this.flipY 420 ); 421 // if (this.animationState === animationStates.idle) { 422 // // if idle for more than 1-4 seconds, go idle animations 423 // } 424 if (this.animationState === animationStates.fly) { 425 let velocity = this.randomizedVelocity(); 426 velocity = normalize(velocity, 4 * multiplier); 427 this.localVelocity = { x: velocity.x, y: velocity.y }; 428 let nextX = this.position.x + velocity.x; 429 let nextY = this.position.y + velocity.y;
430 const potentialIntersection = intersectingObstacle( 431 nextX, 432 nextY, 433 this.width, 434 this.height 435 ); 436 if (potentialIntersection) { 437 if ( 438 this.position.x + this.width < 439 potentialIntersection.left && 440 nextX + this.width > potentialIntersection.left 441 ) { 442 this.clampToObject(RIGHT); 443 nextX = 444 potentialIntersection.left - 445 this.width + 446 clampBleed; 447 nextY = clamp( 448 this.position.y, 449 potentialIntersection.top, 450 potentialIntersection.bottom - this.height 451 ); 452 } else if ( 453 this.position.x > potentialIntersection.right && 454 nextX < potentialIntersection.right 455 ) { 456 this.clampToObject(LEFT); 457 nextX = potentialIntersection.right - clampBleed; 458 nextY = clamp( 459 this.position.y, 460 potentialIntersection.top, 461 potentialIntersection.bottom - this.height 462 ); 463 } else if ( 464 this.position.y + this.height < 465 potentialIntersection.top && 466 nextY + this.height > potentialIntersection.top 467 ) { 468 this.clampToObject(BOTTOM); 469 nextY = 470 potentialIntersection.top - 471 this.height + 472 clampBleed; 473 nextX = clamp( 474 this.position.x, 475 potentialIntersection.left, 476 potentialIntersection.right - this.width 477 ); 478 } else if ( 479 this.position.y > potentialIntersection.bottom && 480 nextY < potentialIntersection.bottom 481 ) { 482 this.clampToObject(TOP); 483 nextY = potentialIntersection.bottom - clampBleed; 484 nextX = clamp( 485 this.position.x, 486 potentialIntersection.left, 487 potentialIntersection.right - this.width 488 ); 489 } 490 this.objectImSittingOn = potentialIntersection; 491 this.switchToIdle(); 492 } 493 // special case the walls as like, thin boxes? 494 if (nextX + this.width > bodyDimensions.width) { 495 nextX = bodyDimensions.width - this.width; 496 this.objectImSittingOn = new DOMRect( 497 bodyDimensions.width, 498 0, 499 10, 500 bodyDimensions.height 501 ); 502 this.clampToObject(RIGHT); 503 this.switchToIdle(); 504 } 505 if (nextX < 0) { 506 nextX = 0; 507 this.objectImSittingOn = new DOMRect( 508 0, 509 0, 510 10, 511 bodyDimensions.height 512 ); 513 this.clampToObject(LEFT); 514 this.switchToIdle(); 515 } 516 if (nextY + this.height > bodyDimensions.height) { 517 nextY = bodyDimensions.height - this.height; 518 this.objectImSittingOn = new DOMRect( 519 0, 520 bodyDimensions.height, 521 bodyDimensions.width, 522 10 523 ); 524 this.clampToObject(BOTTOM); 525 this.switchToIdle(); 526 } 527 if (nextY < 0) { 528 nextY = 0; 529 this.objectImSittingOn = new DOMRect( 530 0, 531 window.scrollY - 10, 532 bodyDimensions.width, 533 10 534 ); 535 // flee when scrolling, since the top has moved 536 window.addEventListener("scroll", this.boundFleeCB); 537 this.clampToObject(TOP); 538 this.switchToIdle(); 539 } 540 this.position.x = nextX; 541 this.position.y = nextY; 542 } else if (this.animationState === animationStates.walk) { 543 switch (this.clampDirection) { 544 case LEFT: 545 this.flipX = this.walkingDirection === 1 ? 1 : 0; 546 this.flipY = 0; 547 break; 548 case RIGHT: 549 this.flipX = this.walkingDirection === -1 ? 1 : 0; 550 this.flipY = 0; 551 break; 552 case TOP: 553 this.flipY = 0; 554 this.flipX = this.walkingDirection === -1 ? 1 : 0; 555 break; 556 case BOTTOM: 557 this.flipY = 0; 558 this.flipX = this.walkingDirection === 1 ? 1 : 0; 559 break; 560 } 561 562 if (this.numberOfWalkingFrames-- > 0) { 563 if ( 564 this.clampDirection === LEFT || 565 this.clampDirection === RIGHT 566 ) { 567 // dont walk into obstacles 568 let nextY = clamp( 569 this.position.y + 570 this.walkingDirection * multiplier, 571 this.objectImSittingOn.top, 572 this.objectImSittingOn.bottom - this.height 573 ); 574 const potentialIntersection = intersectingOb
574stacle( 575 this.position.x, 576 nextY, 577 this.width, 578 this.height, 579 this.objectImSittingOn 580 ); 581 if ( 582 potentialIntersection || 583 nextY <= this.objectImSittingOn.top || 584 nextY + this.height >= this.objectImSittingOn.bottom 585 ) { 586 this.walkingDirection *= -1; 587 this.position.y += 588 (this.walkingDirection * this.height) / 4; 589 } else { 590 this.position.y = nextY; 591 } 592 } else { 593 let nextX = clamp( 594 this.position.x + 595 this.walkingDirection * multiplier, 596 this.objectImSittingOn.left, 597 this.objectImSittingOn.right - this.width 598 ); 599 const potentialIntersection = intersectingObstacle( 600 nextX, 601 this.position.y, 602 this.width, 603 this.height, 604 this.objectImSittingOn 605 ); 606 if ( 607 potentialIntersection || 608 nextX <= this.objectImSittingOn.left || 609 nextX + this.width >= this.objectImSittingOn.right 610 ) { 611 this.walkingDirection *= -1; 612 this.position.x += 613 (this.walkingDirection * this.width) / 4; 614 } else { 615 this.position.x = nextX; 616 } 617 } 618 } else { 619 this.switchToIdle(); 620 } 621 } 622 if ( 623 this.animationState < animationStates.fly && 624 distance(this.position, { x: mouseX, y: mouseY }) < 30 625 ) { 626 this.flee(); 627 } 628 if ( 629 this.animationState < animationStates.fly && 630 window.scrollY > this.position.y 631 ) { 632 this.flee(); 633 } 634 // ctx.drawImage(frame, this.position.x, this.position.y); 635 this.draw( 636 frame, 637 this.position.x, 638 this.position.y, 639 this.flipX, 640 this.flipY 641 ); 642 } 643 } 644 645 const fly = new Fly(); 646 let frameMS = 1000 / 60; 647 let animTimer = 0; 648 const update = () => { 649 requestAnimationFrame(update); 650 let now = performance.now(); 651 let elapsed = now - fly.last; 652 animTimer = 1 - animTimer; 653 if (elapsed > frameMS) { 654 fly.update(elapsed); 655 } 656 }; 657 update(); 658} 659 660if (spriteSheet.complete) { 661 run(); 662} else { 663 spriteSheet.onload = function () { 664 run(); 665 }; 666}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.