JSFiddle - React, Tailwind, and code Playground

https://www.sitepoint.com/community/t/canvas-in-for-loop-rendered-once-and-speed-multiplied/226898/3

by Snj

HTML

<div class="discText"><pre><b>::</b> Sprint animation using requestAnimationFrame.<br><b>::</b> CSS transform zoom and crisp edges.</pre></div>
<div class="image-container">
  <canvas class="case-canvas crisp pixelated scale dimensions" alt=""></canvas>
</div>
<input type="range" id="range" min="1" max="500" value="35">

CSS

body { padding-top: 0px; text-align: center; background: #282827; color: #656565;}

html {
  box-sizing: border-box;
}
*, *::before, *::after {
  box-sizing: inherit;
}
body {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  overflow-x: hidden;
  --size: 35px;
}
.discText {
  position: absolute;
  top: 0px;
}
input {
  position: absolute;
  top: 45px;
  left: 10px;
  width: calc(100% - 20px);
}
.image-container {
  width:  100px;
  height: 100px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 1rem 1rem 2rem;
  position: relative;
  text-align: center;
}
canvas {
  display: block;
}
.scale {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translateX(-50%) translateY(-50%) scale(2.5);
  width: var(--size);
}
.crisp {
  image-rendering: pixelated;
  image-rendering: -moz-crisp-edges;
  image-rendering: crisp-edges;

}

.test-animation {
  -webkit-animation-iteration-count: infinite;
  -webkit-animation-name: pulse;
  -webkit-animation-duration: 3s;
  -webkit-animation-fill-mode: both;
  -webkit-transition-timing-function: ease-in-out;
}
@-webkit-keyframes pulse {
  0% {
    -webkit-transform: scale3d(0.30, 0.30, 0.30);
    -webkit-transform: skewX(-5deg);
    -webkit-transform: skewY(-5deg);
    filter: invert(7%);
  }

  50% {
    -webkit-transform: scale3d(3.50, 3.50, 3.50);
    -webkit-transform: skewX(0deg);
    -webkit-transform: skewY(0deg);
    filter: contrast(0px);
    opacity: 1;
    filter: invert(0%);

  }

  100% {
    -webkit-transform: scale3d(0.30, 0.30, 0.30);
    -webkit-transform: skewX(-5deg);
    -webkit-transform: skewY(-5deg);
    filter: invert(7%);
  }
}

JavaScript

range.addEventListener("input", function() {
  document.body.style.setProperty("--size", this.value + "px");
});

var monster, monsterImg, canvas;

function gameLoop () {
  requestAnimationFrame(gameLoop);
  monster.update();
  monster.render();
}

function sprite (options) {
  var that = {},
      frameIndex = 0,
      tickCount = 0,
      ticksPerFrame = options.ticksPerFrame || 0,
      numberOfFrames = options.numberOfFrames || 1;

  that.context = options.context;
  that.width = options.width;
  that.height = options.height;
  that.image = options.image;

  that.update = function () {
    tickCount += 1;

    if (tickCount > ticksPerFrame) {
      tickCount = 0;

      // If the current frame index is in range
      if (frameIndex < numberOfFrames - 1) {
        // Go to the next frame
        frameIndex += 1;
      } else {
        frameIndex = 0;
      }
    }
  };

  that.render = function () {
    // Clear the canvas
    that.context.clearRect(0, 0, that.width, that.height);

    // Draw the animation
    that.context.drawImage(
      that.image,
      frameIndex * that.width / numberOfFrames,
      0,
      that.width / numberOfFrames,
      that.height,
      0,
      0,
      that.width / numberOfFrames,
      that.height
    );
  };

  return that;
}

canvas = document.querySelector('.case-canvas');
canvas.width = 100;
canvas.height = 150;

monsterImg = new Image();

monster = sprite({
  context: canvas.getContext("2d"),
  width: 18693,
  height: 150,
  image: monsterImg,
  numberOfFrames: 201,
  ticksPerFrame: 1
});

monsterImg.addEventListener("load", gameLoop);
monsterImg.src = "https://i.imgur.com/inXxo8L.png";
// http://192.168.0.104/s-3d-sprite_v2.png