title: Running a separate canvas animation to the game loop animation
body: <p>I'm caught in a logic knot with this game :(. I simply want to remove the explosions from the screen after say 1 second of each doing its loop. As you can see below they run at the frame rate of the game loop. This is the only way I could animate the explosion - by setting a sprite to move at the speed of the game loop (frame rate).</p>
  <p>I don't understand how to connect a separate animation going at a different speed to this same canvas context that is being essentially cleared every frame.. I can't even figure out how to stop the explosion sprite loop.</p>
  <p>I've tried creating a separate method <code>drawExplosion()</code> in the Explosion class and using <code>setInterval</code> in the Explosion constructor but it never likes the context I connect it to and throws this error:</p>
  <pre><code> Cannot read property 'drawImage' of undefined (i.e. the context is undefined)
  </code></pre>
  <p><strong>If someone could just stop each explosion loop after 1 second I would understand where I went off course</strong></p>
  <p>The outline of the code is this:</p>
  <pre><code>class Entity
  class Ball extends Entity
  class Explosion extends Entity
  class Enemy extends Entity
  class Paddle extends Entity
  class InputsManager
  class mouseMoveHandler
  class Game
  
  const canvas = document.querySelector('canvas')
  const game = new Game(canvas)
  game.start()
  </code></pre>
  <p><div class="snippet" data-lang="js" data-hide="false" data-console="true" data-babel="false">
  <div class="snippet-code">
  <pre class="snippet-code-html lang-html prettyprint-override"><code>&lt;!DOCTYPE html&gt;
  &lt;html lang="en"&gt;
  
  &lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;Document&lt;/title&gt;
    &lt;style&gt;
      body {
        background-color: rgb(214, 238, 149);
        height: 100vh;
        display: flex;
        justify-content: center;
        align-items: center;
        margin: 0;
        padding: 0;
      }
  
      canvas {
        background: url("https://picsum.photos/200");
        width: 100%;
        background-size: cover;
      }
    &lt;/style&gt;
  &lt;/head&gt;
  
  &lt;body&gt;
  
    &lt;canvas height="459"&gt;&lt;/canvas&gt;
  &lt;/body&gt;
  
  &lt;script&gt;
    class Entity {
      constructor(x, y) {
        this.dead = false;
        this.collision = 'none'
        this.x = x
        this.y = y
      }
  
      update() { console.warn(`${this.constructor.name} needs an update() function`) }
      draw() { console.warn(`${this.constructor.name} needs a draw() function`) }
      isDead() { console.warn(`${this.constructor.name} needs an isDead() function`) }
  
      static testCollision(a, b) {
        if (a.collision === 'none') {
          console.warn(`${a.constructor.name} needs a collision type`)
          return undefined
        }
        if (b.collision === 'none') {
          d
          console.warn(`${b.constructor.name} needs a collision type`)
          return undefined
        }
        if (a.collision === 'circle' &amp;&amp; b.collision === 'circle') {
          return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2) &lt; a.radius + b.radius
        }
        if (a.collision === 'circle' &amp;&amp; b.collision === 'rect' || a.collision === 'rect' &amp;&amp; b.collision === 'circle') {
          let circle = a.collision === 'circle' ? a : b
          let rect = a.collision === 'rect' ? a : b
          // this is a waaaaaay simplified collision that just works in this case (circle always comes from the bottom)
          const topOfBallIsAboveBottomOfRect = circle.y - circle.radius &lt;= rect.y + rect.height
          const bottomOfBallIsBelowTopOfRect = circle.y + circle.radius &gt;= rect.y - rect.height
          const ballIsRightOfRectLeftSide = circle.x + circle.radius &gt;= rect.x - rect.width / 4
          const ballIsLeftOfRectRightSide = circle.x - circle.radius &lt;= rect.x + rect.width
          return topOfBallIsAboveBottomOfRect &amp;&amp; bottomOfBallIsBelowTopOfRect &amp;&amp; ballIsRightOfRectLeftSide &amp;&amp; ballIsLeftOfRectRightSide
        }
        console.warn(`there is no collision function defined for a ${a.collision} and a ${b.collision}`)
        return undefined
      }
  
      static testBallCollision(ball) {
        const topOfBallIsAboveBottomOfRect = ball.y - ball.radius &lt;= this.y + this.height / 2
        const bottomOfBallIsBelowTopOfRect = ball.y + ball.radius &gt;= this.y - this.height / 2
        const ballIsRightOfRectLeftSide = ball.x - ball.radius &gt;= this.x - this.width / 2
        const ballIsLeftOfRectRightSide = ball.x + ball.radius &lt;= this.x + this.width / 2
        return topOfBallIsAboveBottomOfRect &amp;&amp; bottomOfBallIsBelowTopOfRect &amp;&amp; ballIsRightOfRectLeftSide &amp;&amp; ballIsLeftOfRectRightSide
      }
    }
  
  
    class Ball extends Entity {
      constructor(x, y) {
        super(x, y)
        this.dead = false;
        this.collision = 'circle'
        this.speed = 300 // px per second
        this.radius = 2.5 // radius in px
        this.color = '#fff'
        this.ballsDistanceY = 12
      }
  
      update({ deltaTime }) {
        // Ball still only needs deltaTime to calculate its update
        this.y -= this.speed * deltaTime / 1000 // deltaTime is ms so we divide by 1000
      }
  
      /** @param {CanvasRenderingContext2D} context */
      draw(context) {
        context.beginPath()
        context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI)
        context.fillStyle = this.color;
        context.fill()
  
        context.beginPath()
        context.arc(this.x, this.y + this.ballsDistanceY, this.radius, 0, 2 * Math.PI)
        context.fillStyle = this.color;
        context.fill()
  
        context.beginPath()
        context.arc(this.x, this.y - this.ballsDistanceY, this.radius, 0, 2 * Math.PI)
        context.fillStyle = this.color;
        context.fill()
      }
  
      isDead(enemy) {
        const outOfBounds = this.y &lt; 0 - this.radius
        const collidesWithEnemy = Entity.testCollision(enemy, this)
  
        if (outOfBounds) {
          return true
        }
        if (collidesWithEnemy) {
          //console.log('dead')
          this.dead = true;
          game.hitEnemy();
          return true
        }
      }
    }
  
  
    class Explosion extends Entity {
      constructor(x, y, contextFromGameObject){
        super(x, y)
        this.contextFromGameObject = contextFromGameObject
        this.imgExplosion = new Image();
        this.imgExplosion.src = "https://i.ibb.co/9Ggfzxr/explosion.png";
        this.totalNumberOfFrames = 12 // ten images in the image (see the url above)
        this.spriteFrameNumber = 0 // This is changed to make the sprite animate  
        this.widthOfSprite = 1200 // this.imgExplosion.width; // find the width of the image
        this.heightOfSprite = 100 // this.imgExplosion.height; // find the height of the image
        this.widthOfSingleImage = this.widthOfSprite / this.totalNumberOfFrames; // The width of each image in the spirite
        //this.timerId = setInterval(this.explode.bind(this), 100)
        this.scaleExplosion = 0.5
       
        //this.timerId = setInterval(this.drawExplosion, 100);
      }
  
      // drawExplosion(){
      //   console.log(this.spriteFrameNumber)
  
      //   //ctx.clearRect(0, 0, 500, 500)
      //   this.spriteFrameNumber += 1; // changes the sprite we look at
      //   this.spriteFrameNumber = this.spriteFrameNumber % this.totalNumberOfFrames; // Change this from 0 to 1 to 2 ... upto 9 and back to 0 again, then 1...
        
      //   this.contextFromGameObject.drawImage(this.imgExplosion,
      //     this.spriteFrameNumber * this.widthOfSingleImage, 0, // x and y - where in the sprite
      //     this.widthOfSingleImage, this.heightOfSprite, // width and height
      //     this.x - 25, this.y - 25, // x and y - where on the screen
      //     this.widthOfSingleImage, this.heightOfSprite // width and height
      //   );
  
      //   if (this.spriteFrameNumber &gt; 9) {
      //     clearInterval(this.timerId)
      //   };
      // }
  
      /** @param {CanvasRenderingContext2D} context */
      draw(context, frameNumber) {
        console.log(frameNumber)
      
        //ctx.clearRect(0, 0, 500, 500)
        this.spriteFrameNumber += 1; // changes the sprite we look at
        this.spriteFrameNumber = this.spriteFrameNumber % this.totalNumberOfFrames; // Change this from 0 to 1 to 2 ... upto 9 and back to 0 again, then 1...
        
        context.drawImage(this.imgExplosion,
          this.spriteFrameNumber * this.widthOfSingleImage, 0, // x and y - where in the sprite
          this.widthOfSingleImage, this.heightOfSprite, // width and height
          this.x - 25, this.y - 25, // x and y - where on the screen
          this.widthOfSingleImage, this.heightOfSprite // width and height
        );
      }
   
  
      update() {
      }
  
      isDead(ball, isDead) {
        if(isDead == 'true'){
          clearTimeout(this.timerId);
          return true
        }
        return false
      }
    } 
  
  
    class Enemy extends Entity {
      constructor(x, y) {
        super(x, y)
        this.collision = 'rect'
        this.height = 50;
        this.width = 50;
        this.speedVar = 4;
        this.speed = this.speedVar;
        this.color = '#EC3AC8';
        this.color2 = '#000000';
        this.y = y;
        this.imgEnemy = new Image();
        this.imgEnemy.src = "https://i.ibb.co/kgXsr66/question.png";
      
        this.runCount = 1;
        this.timerId = setInterval(this.movePosish.bind(this), 1000);
      }
  
      movePosish() {
        //console.log(this.runCount)
  
        // x 10 -&gt; 240
        // y 10 -&gt; 300
        switch (this.runCount) {
          case 0:
            this.x = 20; this.y = 200;
            break
          case 1:
            this.x = 200; this.y = 300;
            break
          case 2:
            this.x = 30; this.y = 20;
            break
          case 3:
            this.x = 230; this.y = 150;
            break
          case 4:
            this.x = 200; this.y = 20;
            break
          case 5:
            this.x = 30; this.y = 90;
            break
          case 6:
            this.x = 240; this.y = 20;
            break
          case 7:
            this.x = 30; this.y = 150;
            break
          case 8:
            this.x = 180; this.y = 170;
            break
          case 9:
            this.x = 30; this.y = 50;
            break
          case 10:
            this.x = 130; this.y = 170;
            break
        }
  
        //if 10th image remove image and clear timer
        this.runCount += 1;
        if (this.runCount &gt; 10) {
          //clearInterval(this.timerId)
          this.runCount = 0;
          console.log('ya missed 10 of em')
        };
      }
  
      update() {
        // //Moving left/right
        // this.x += this.speed;
        // if (this.x &gt; canvas.width - this.width) {
        //   this.speed -= this.speedVar;
        // }
        // if (this.x === 0) {
        //   this.speed += this.speedVar;
        // }
  
      }
  
      /** @param {CanvasRenderingContext2D} context */
      draw(context) {
        // context.beginPath();
        // context.rect(this.x, this.y, this.width, this.height);
        // context.fillStyle = this.color2;
        // context.fill();
        // context.closePath();
  
        context.drawImage(this.imgEnemy, this.x, this.y);
      }
  
      isDead(enemy) {
        //// collision detection 
        // const collidesWithEnemy = Entity.testCollision(enemy, ball)
        // if (collidesWithEnemy){
        //   console.log('enemy dead')
        //   game.hitEnemy();
        //   return true
        // }
  
        // if (ball.dead){
        //   console.log('enemy dead')
        //   game.hitEnemy();
        //   return true
        // }
        return false
      }
    }
  
  
    class Paddle extends Entity {
      constructor(x, width) {
        super(x, width)
        this.collision = 'rect'
        this.speed = 200
        this.height = 25
        this.width = 30
        this.color = "#74FCEF"
      }
  
      update({ deltaTime, inputs, mouse }) {
        // Paddle needs to read both deltaTime and inputs
        // if mouse inside canvas AND not on mobile
  
        if (mouse.insideCanvas) {
          this.x = mouse.paddleX
        } else {
          this.x += this.speed * deltaTime / 1000 * inputs.direction
          // stop from going off screen
          if (this.x &lt; this.width / 2) {
            this.x = this.width / 2;
          } else if (this.x &gt; canvas.width - this.width / 2) {
            this.x = canvas.width - this.width / 2
          }
  
        }
      }
  
      /** @param {CanvasRenderingContext2D} context */
      draw(context) {
        context.beginPath();
        context.rect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height);
        context.fillStyle = this.color;
        context.fill();
        context.closePath();
  
        context.beginPath();
        context.rect(this.x - this.width / 12, this.y - this.height / 1.1, this.width / 6, this.height);
        context.fillStyle = this.color;
        context.fill();
        context.closePath();
      }
      isDead() { return false }
    }
  
  
    class InputsManager {
      constructor() {
        this.direction = 0 // this is the value we actually need in out Game object
        window.addEventListener('keydown', this.onKeydown.bind(this))
        window.addEventListener('keyup', this.onKeyup.bind(this))
      }
  
      onKeydown(event) {
        switch (event.key) {
          case 'ArrowLeft':
            this.direction = -1
            break
          case 'ArrowRight':
            this.direction = 1
            break
        }
      }
  
      onKeyup(event) {
        switch (event.key) {
          case 'ArrowLeft':
            if (this.direction === -1) // make sure the direction was set by this key before resetting it
              this.direction = 0
            break
          case 'ArrowRight':
            this.direction = 1
            if (this.direction === 1) // make sure the direction was set by this key before resetting it
              this.direction = 0
            break
        }
      }
    }
  
  
    class mouseMoveHandler {
      constructor() {
        // this.paddleWidth = paddleWidth;
        this.x = 0;
        this.paddleX = 0;
        //this.canvas = canvas;
        document.addEventListener("mousemove", this.onMouseMove.bind(this), false);
      }
  
      //'relative to canvas width' mouse position snippet
      getMousePos(canvas, evt) {
        var rect = canvas.getBoundingClientRect(), // abs. size of element
          scaleX = canvas.width / rect.width,    // relationship bitmap vs. element for X
          scaleY = canvas.height / rect.height;  // relationship bitmap vs. element for Y
        //console.log('canvas width = ' + canvas.width)
        return {
          x: (evt.clientX - rect.left) * scaleX,   // scale mouse coordinates after they have
          y: (evt.clientY - rect.top) * scaleY     // been adjusted to be relative to element
        }
      }
  
      onMouseMove(e) {
        //console.log('moving')
        //this.x = 100;
        this.x = this.getMousePos(canvas, e).x; //relative x on canvas
        this.y = this.getMousePos(canvas, e).y; //relative x on canvas
        this.insideCanvas = false;
  
        if (this.x &gt; 0 &amp;&amp; this.x &lt; canvas.width) {
          if (this.y &gt; 0 &amp;&amp; this.y &lt; canvas.height) {
            //console.log('inside')
            this.insideCanvas = true;
          } else {
            this.insideCanvas = false;
          }
        }
  
        if (this.x - 20 &gt; 0 &amp;&amp; this.x &lt; canvas.width - 20) {
          this.paddleX = this.x;
        }
      }
    }
  
  
    class Game {
      /** @param {HTMLCanvasElement} canvas */
      constructor(canvas) {
        this.entities = [] // contains all game entities (Balls, Paddles, ...)
        this.context = canvas.getContext('2d')
        this.newBallInterval = 900 // ms between each ball
        this.lastBallCreated = -Infinity // timestamp of last time a ball was launched
        this.paddleWidth = 50
        this.isMobile = false
        this.frameNumber = 0;
      }
  
      endGame() {
        //clear all elements, remove h-hidden class from next frame, then remove h-hidden class from the cta content
        console.log('endgame')
        const endGame = true;
        game.loop(endGame)
      }
  
      hitEnemy() {
        //this.timerId = setTimeout(endExplosion(), 500);
        this.explosion = new Explosion(this.enemy.x, this.enemy.y, this.context)
        this.entities.push(this.explosion)
      }
  
      start() {
        this.lastUpdate = performance.now()
  
        this.enemy = new Enemy(140, 220)
        this.entities.push(this.enemy)
  
        // we store the new Paddle in this.player so we can read from it later
        this.player = new Paddle(150, 400)
        // but we still add it to the entities list so it gets updated like every other Entity
        this.entities.push(this.player)
  
        //start watching inputs
        this.inputsManager = new InputsManager()
  
        //start watching mousemovement
        this.mouseMoveHandler = new mouseMoveHandler()
  
        //start game loop
        this.loop()
      }
  
      update() {
        // calculate time elapsed
        const newTime = performance.now()
        const deltaTime = newTime - this.lastUpdate
        this.isMobile = window.matchMedia('(max-width: 1199px)');
  
  
        // we now pass more data to the update method so that entities that need to can also read from our InputsManager
        const frameData = {
          deltaTime,
          inputs: this.inputsManager,
          mouse: this.mouseMoveHandler,
          context: this.context
        }
  
        // update every entity
        this.entities.forEach(entity =&gt; entity.update(frameData))
  
        // other update logic (here, create new entities)
        if (this.lastBallCreated + this.newBallInterval &lt; newTime) {
          // this is quick and dirty, you should put some more thought into `x` and `y` here
          this.ball = new Ball(this.player.x, 360)
          this.entities.push(this.ball)
          this.lastBallCreated = newTime
        }
  
        // remember current time for next update
        this.lastUpdate = newTime
  
        
      }
  
      cleanup() {
        //console.log(this.entities[0])//Enemy
        //console.log(this.entities[1])//Paddle
        //console.log(this.entities[2])//Ball
        //to prevent memory leak, don't forget to cleanup dead entities
        this.entities.forEach(entity =&gt; {
          if (entity.isDead(this.enemy)) {
            const index = this.entities.indexOf(entity)
            this.entities.splice(index, 1)
          }
        })
      }
  
      draw() {
        //draw entities
        this.entities.forEach(entity =&gt; entity.draw(this.context, this.frameNumber))
      }
  
      //main game loop
      loop(endGame) {
        this.myLoop = requestAnimationFrame(() =&gt; {
          this.frameNumber += 1;
          this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height)
  
          if (endGame) {
            cancelAnimationFrame(this.myLoop);
            this.endGame()
            return;
          }
  
          this.update()
          this.draw()
          this.cleanup()
          this.loop()
        })
      }
    }
  
    const canvas = document.querySelector('canvas')
    const game = new Game(canvas)
    game.start()
  
  &lt;/script&gt;
  
  &lt;/html&gt;</code></pre>
  </div>
  </div>
  </p>