the-world.ts (3903B)
1 document.addEventListener("DOMContentLoaded", () => { 2 const containerEl = document.querySelector( 3 "#contains-the-world", 4 ) as HTMLSpanElement; 5 const worldEl = document.querySelector("#the-world") as HTMLDivElement; 6 const spriteWidth = worldEl.getBoundingClientRect().width; 7 const initForce = 1; 8 const maxVelocity = 100; 9 let force = initForce; 10 const accel = 0.1; 11 const degreesInRotation = 360; 12 const frames = 12; 13 const degreesInFrame = degreesInRotation / frames; 14 const fps = 24; 15 const tickTime = 1000 / fps; 16 17 // targets 3 sprite frames per sec 18 const steadyIncrease = (degreesInFrame * 3) / fps; 19 20 const xOffset = (frame: number) => { 21 return frame * spriteWidth; 22 }; 23 24 let rotation = 0; // in degrees 25 let frame = 0; 26 let velocity = 0; 27 28 const physicsTime = 3_000; 29 const esplodeTime = 100_000; 30 31 const isTimeFor = (ts, tTimer, tgtTime) => { 32 return ts - tTimer > tgtTime; 33 }; 34 35 const drag = (v: number) => Math.max(v / 10, 0.005 * v * v); 36 37 const updatePosition = (v = velocity) => { 38 rotation = (rotation + v) % degreesInRotation; 39 frame = Math.floor(rotation / degreesInFrame); 40 }; 41 42 const updateVelocity = (deltaV: number) => { 43 velocity = Math.min( 44 maxVelocity, 45 velocity + deltaV - drag(Math.round(velocity)), 46 ); 47 }; 48 49 const physicsTick = (isHovering: boolean) => { 50 updateVelocity(isHovering ? force : 0); 51 updatePosition(); 52 53 if (isHovering) { 54 force += accel; 55 } else { 56 force = initForce; 57 } 58 59 if (velocity > 45) { 60 shakeTick(); 61 } else { 62 resetShake(); 63 } 64 65 updateFrame(); 66 }; 67 68 const steadyTick = (isHovering: boolean) => { 69 if (isHovering) { 70 updatePosition(steadyIncrease); 71 updateFrame(); 72 } 73 }; 74 75 const updateFrame = () => { 76 worldEl.style.backgroundPositionX = `${xOffset(frame)}px`; 77 }; 78 79 const rBigPos = () => 80 (Math.random() > 0.5 ? 1 : -1) * (2000 + Math.random() * 2000); 81 82 const esplode = () => { 83 console.log("boom!"); 84 const pos = worldEl.getBoundingClientRect(); 85 worldEl.style.position = "fixed"; 86 worldEl.style.top = pos.y + "px"; 87 worldEl.style.left = pos.x + "px"; 88 const dest = `translate(${rBigPos()}px, ${rBigPos()}px) scale(${Math.random() > 0.5 ? 20 : 0.001})`; 89 worldEl.style.transform = dest; 90 }; 91 92 let shakeDir = 1; 93 let shakeTimer: number | null = null; 94 const shakeTick = () => { 95 if (!shakeTimer) { 96 shakeTimer = setTimeout(() => { 97 esplode(); 98 }, esplodeTime); 99 } 100 shakeDir *= -1; 101 worldEl.style.translate = `${shakeDir}px`; 102 }; 103 104 const resetShake = () => { 105 if (shakeTimer !== null) { 106 clearTimeout(shakeTimer); 107 } 108 worldEl.style.translate = ""; 109 }; 110 111 let t = -Infinity; 112 let tPhysics: number | null = null; 113 let inPhysics = false; 114 let doAnimation = false; 115 const animate = (timestamp: DOMHighResTimeStamp = 0) => { 116 if (!doAnimation) { 117 return; 118 } 119 120 if (!tPhysics) { 121 tPhysics = timestamp; 122 } 123 if (isTimeFor(timestamp, t, tickTime)) { 124 t = timestamp; 125 126 const worldHovering = worldEl.matches(":hover"); 127 const textHovering = containerEl.matches(":hover"); 128 const isHovering = worldHovering || textHovering; 129 130 // physics timer only run while hovering on world. 131 if (!worldHovering) { 132 tPhysics = timestamp; 133 } 134 135 inPhysics = 136 (worldHovering && isTimeFor(timestamp, tPhysics, physicsTime)) || 137 velocity > 0.5; 138 139 // if (!inPhysics && !isHovering) { 140 // doAnimation = false; 141 // return; 142 // } 143 144 if (!inPhysics) { 145 steadyTick(true); 146 } else { 147 physicsTick(worldHovering); 148 } 149 } 150 requestAnimationFrame(animate); 151 }; 152 153 doAnimation = true; 154 animate(); 155 156 containerEl.addEventListener("mouseenter", () => { 157 if (!doAnimation) { 158 doAnimation = true; 159 animate(); 160 } 161 }); 162 });