]> git.ardis.lu - ard.is/commitdiff
Use pre-generated frames instead of setTimeout
authorArdis Lu <ardisglu@gmail.com>
Sun, 11 Jun 2023 03:30:05 +0000 (20:30 -0700)
committerArdis Lu <ardisglu@gmail.com>
Sun, 11 Jun 2023 03:30:05 +0000 (20:30 -0700)
Fixes final frame race condition and reduces lag

index.html

index 76312ae89e1b94b31d1c3d73eda39098cf0c10b3..a6b3a545e2d87c90b64e86b612140c3511a5ff7b 100644 (file)
@@ -29,9 +29,9 @@
   <script type="module">
     const asciiContainer = document.getElementById('ascii-container');
 
-    const timePerTick = 200; // In ms
-    const maxTicks = 25; // 25 ticks * 200 ms = maximum 5000ms total cycle time
-    const cycle = '<>?/|\\[]{}()-_~`'; // Characters to cycle through randomly before settling
+    const msPerFrame = 50; // 1000ms / 20fps = 50
+    const totalFrames = 100; // 50 * 100 = 5s animation duration
+    const cycle = '<>?\\[]{}-~`*+=.!@#$%^&'; // Characters to cycle through randomly before settling
     const ascii =
 `               _          
               | |  (_)    
 | |_| | | | |_| |  | |__ |
 |_____|_| |_____|(_)_|___|`;
 
-    // 4 random numbers are required for each character
-    const rng = [...crypto.getRandomValues(new Uint8Array(ascii.length * 4))];
-
-    for (const finalChar of ascii) {
-      if (finalChar === '\n') {
-        asciiContainer.insertAdjacentHTML('beforeend', '<br>');
-        continue;
-      }
-
-      const startOffset = rng.pop(); // Initial delay before starting the cycle (implied max = 255ms)
-      const jump = rng.pop() % cycle.length; // How many cycle symbols to skip per tick
-      let cycleIndex = rng.pop() % cycle.length; // Which character to begin with in the cycle
-      let ticksRemaining = rng.pop() % maxTicks; // Number of ticks before settling
-
-      const charElem = document.createElement('span');
-      asciiContainer.insertAdjacentElement('beforeend', charElem);
-
-      function tick() {
-        if (ticksRemaining === 0) {
-          charElem.replaceWith(finalChar);
-          return;
+    // Pre-generate all frames for the animation 
+    const rng = [...crypto.getRandomValues(new Uint8Array(ascii.length * totalFrames))];
+    const frames = [ascii.replaceAll(/[^\n]/g, () => cycle[rng.pop() % cycle.length])]; // Initialize first frame with random symbols
+    for (let i = 1; i < totalFrames - 1; i++) { // First and last frames are hardcoded
+      let frame = '';
+      for (const [j, finalChar] of [...ascii].entries()) {
+        const rand = rng.pop();
+        if (frames[i - 1][j] === finalChar || rand < 13) { // Character is already settled, or 5% chance of settling (13/256)
+          frame += finalChar;
+        }
+        else if (rand < 64) { // 20% chance of switching: (64 - 13)/256
+          frame += cycle[rand % cycle.length];
         }
-        charElem.textContent = cycle[cycleIndex];
-        cycleIndex = (cycleIndex + jump) % cycle.length;
-        ticksRemaining--;
-        setTimeout(tick, timePerTick);
-      };
-      setTimeout(tick, startOffset);
+        else { // 75% chance of staying the same as last frame
+          frame += frames[i - 1][j];
+        }
+      }
+      frames.push(frame);
     }
+    frames.push(ascii); // Final frame
 
-    // Flatten all inner text nodes into one text node in the outer container
-    setTimeout(() => asciiContainer.textContent = asciiContainer.innerText, timePerTick * (maxTicks + 1));
+    // Start animation
+    let start;
+    function step(timestamp) {
+      if (start === undefined) {
+        start = timestamp;
+      }
+      const currentFrame = Math.floor((timestamp - start) / msPerFrame);
+      if (currentFrame >= frames.length) {
+        return;
+      }
+      asciiContainer.textContent = frames[currentFrame];
+      requestAnimationFrame(step);
+    }
+    requestAnimationFrame(step);
   </script>
 </body>
 
This page took 0.036972 seconds and 4 git commands to generate.