javascriptrequestanimationframecancelanimationframe

Can't cancel requestAnimationFrame and even run multiple animation simultaneously


I have an animation and it will jump into the target height, and then fall to the ground.

If I click "jump" twice when it is still in the air and does not hit the target height, it will recalculate the target and continue the jump into the new target. This event works.

But if I click "jump" when the object is going down, it should cancel the "fall" animation, and retrigger the "jump" animation again into the new target. However, this does not work. When I click, the animation stops.

And so I try to debug. I put a console.log on each function to see what's happening.

When the object jumps and falls, it shows this:

enter image description here

But if I click jump again when it's falling, it shows this:

enter image description here

It seems that when I click the jump again, it's not canceling the "fall" animation frame, so the jump and fall functions repeat simultaneously infinitely. So I assume the window.cancelAnimationFrame() is not working.

How to fix/properly cancel an animation?

Svelte version example for simplicity: https://svelte.dev/repl/dcf5913d7b75422aaa59123286591fd9?version=4.1.1

let up = false
let height = 0
let speed = 1
let target = height + 150
let rect = document.getElementById("rect")
let stateText = document.getElementById("state")
let upText = document.getElementById("up")
let heightText = document.getElementById("height")
let targetText = document.getElementById("target")

stateText.innerText = "State: idle"
upText.innerText = "Up: "
heightText.innerText = "Height: 0"
targetText.innerText = "Target: " + target


function jump() {
  up = true
  upText.innerText = "Up: " + up
  stateText.innerText = "State: jump"
  if (height < target) {
    // before hit the target height
    console.log("debug jump")
    height += speed
    heightText.innerText = "Height: " + height
    rect.setAttribute("y", 280 - height)
    window.requestAnimationFrame(jump)
  } else {
    // falls down after hitting the target height
    window.cancelAnimationFrame(jump)
    up = false
    window.requestAnimationFrame(fall)
  }
}

function fall() {
  up = false
  stateText.innerText = "State: fall"
  upText.innerText = "Up: " + up
  if (height > -1) {
    // falls down
    console.log("debug fall")
    height -= speed
    heightText.innerText = "Height: " + height
    rect.setAttribute("y", 280 - height)
    window.requestAnimationFrame(fall)
  } else {
    // stays on the ground when the falls finish
    window.cancelAnimationFrame(fall)
    height = 0
    stateText.innerText = "State : idle"
    heightText.innerText = "Height: " + height
    up = false
  }
}

function handleClick() {
  target = height + 150
  targetText.innerText = "Target: " + target
  if (!up) {
    // if the object falls, cancel fall animation
    // and trigger the jump animation
    console.log("cancel fall")
    window.cancelAnimationFrame(fall)
    window.requestAnimationFrame(jump)
  }
}
svg {
  border: solid;
  width: 300px;
  height: 300px;
}

h3 {
  margin: 0;
}
<h3 id="state"></h3>
<h3 id="up"></h3>
<h3 id="height"></h3>
<h3 id="target"></h3>

<button id="button" onClick="handleClick()">Jump</button>
<br><br>
<svg>
    <rect id="rect" x="140" y="280" width="20" height="20"></rect>
</svg>


Solution

  • You seem to be using cancelAnimationFrame() incorrectly. As per the documentation, the parameter you pass should be "the ID value returned by the call to window.requestAnimationFrame() that requested the callback."

    Thus, you should have a reference to the request ID returned from your requestAnimationFrame() calls and then pass this request ID to cancelAnimationFrame() when needed:

    let up = false
    let height = 0
    let speed = 1
    let target = height + 150
    let rect = document.getElementById("rect")
    let stateText = document.getElementById("state")
    let upText = document.getElementById("up")
    let heightText = document.getElementById("height")
    let targetText = document.getElementById("target")
    
    stateText.innerText = "State: idle"
    upText.innerText = "Up: "
    heightText.innerText = "Height: 0"
    targetText.innerText = "Target: " + target
    
    let animationId;
    
    function jump() {
      up = true
      upText.innerText = "Up: " + up
      stateText.innerText = "State: jump"
      if (height < target) {
        // before hit the target height
        console.log("debug jump")
        height += speed
        heightText.innerText = "Height: " + height
        rect.setAttribute("y", 280 - height)
        animationId = window.requestAnimationFrame(jump)
      } else {
        // falls down after hitting the target height
        window.cancelAnimationFrame(animationId)
        up = false
        animationId = window.requestAnimationFrame(fall)
      }
    }
    
    function fall() {
      up = false
      stateText.innerText = "State: fall"
      upText.innerText = "Up: " + up
      if (height > -1) {
        // falls down
        console.log("debug fall")
        height -= speed
        heightText.innerText = "Height: " + height
        rect.setAttribute("y", 280 - height)
        animationId = window.requestAnimationFrame(fall)
      } else {
        // stays on the ground when the falls finish
        window.cancelAnimationFrame(animationId)
        height = 0
        stateText.innerText = "State : idle"
        heightText.innerText = "Height: " + height
        up = false
      }
    }
    
    function handleClick() {
      target = height + 150
      targetText.innerText = "Target: " + target
      if (!up) {
        // if the object falls, cancel fall animation
        // and trigger the jump animation
        console.log("cancel fall")
        window.cancelAnimationFrame(animationId)
        animationId = window.requestAnimationFrame(jump)
      }
    }
    svg {
      border: solid;
      width: 300px;
      height: 300px;
    }
    
    h3 {
      margin: 0;
    }
    <h3 id="state"></h3>
    <h3 id="up"></h3>
    <h3 id="height"></h3>
    <h3 id="target"></h3>
    
    <button id="button" onClick="handleClick()">Jump</button>
    <br><br>
    <svg>
        <rect id="rect" x="140" y="280" width="20" height="20"></rect>
    </svg>