Mikołaj Walanus
Mikołaj Walanus

Reputation: 33

How to cancel current animation and immediately start new one with mouse event and requestAnimationFrame()

I want the dot to follow mouse cursor, e.g. on click. Code seems simple, but with every click the dot runs shorter distance and doesn't reach the target.

The question is why?

The code is here:

https://jsfiddle.net/thiefunny/ny0chx3q/3/

HTML

<circle r="10" cx="300" cy="300" />

JavaScript

const circle = document.querySelector("circle")

window.addEventListener("click", mouse => {

    const animation = _ => {

        let getCx = Number(circle.getAttribute('cx'))
        let getCy = Number(circle.getAttribute('cy'))

        circle.setAttribute("cx", `${getCx + (mouse.clientX - getCx)/10}`);
        circle.setAttribute("cy", `${getCy + (mouse.clientY - getCy)/10}`);

        requestAnimationFrame(animation)

    }

    requestAnimationFrame(animation)

});

EDIT: for this task I need requestAnimationFrame(), not CSS, because this is just the simplest example, but I want to add much more complexity later to the movement, including multiple dots, random parametres etc., like I did here: https://walanus.pl

I spent lots of time experimenting, but the only conclusion I have is that after click event I should somehow cancel the current animation and start new one to make a fresh start for next animation.

Upvotes: 0

Views: 1670

Answers (2)

n--
n--

Reputation: 3856

you don't need requestAnimationFrame for this:

const circle = document.querySelector("circle")

window.addEventListener("click", e => {

  let targetCircleX = e.clientX;
  let targetCircleY = e.clientY;
  let getCx = Number(circle.getAttribute('cx'))
  let getCy = Number(circle.getAttribute('cy'))
  let cx = targetCircleX - getCx;
  let cy = targetCircleY - getCy;
  
  circle.style.transform = `translate3d(${cx}px, ${cy}px, 0)`;
  
});
circle {
  transition-duration: .2s;
}
<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="500" height="500">
  <circle r="10" cx="100" cy="100" />
</svg>
    

EDIT: CSS animations are an easy yet powerful method to animate things in web, but manual control over the animation, done properly, always require more work, i.e. performant loops, proper timings, etc. (by the way, the mentioned site doesn't bother with these). So, for fullness of answer, a variant with requestAnimationFrame is below

const circle = document.querySelector("circle");

const fps = 60;
const delay = 1000 / fps;
let rafId;

window.addEventListener("click", e => {

    cancelAnimationFrame(rafId);

    let [time, cx, cy, xf, yf] = [0];
    let r = +circle.getAttribute('r');
    let [X, x] = [e.clientX - r, +circle.getAttribute('cx')];
    let [Y, y] = [e.clientY - r, +circle.getAttribute('cy')];
    const decel = 10;
    
    const anim = now => {
        const delta = now - time;
        if (delta > delay) {
            time = now - (delta % delay);
            [x, y] = [x + (X - x) / decel, y + (Y - y) / decel];
            [xf, yf] = [x.toFixed(1), y.toFixed(1)];
            if (cx === xf && cy === yf)
                return;
            circle.setAttribute("cx", cx = xf);
            circle.setAttribute("cy", cy = yf);
        }
        rafId = requestAnimationFrame(anim);
    }

    anim(time);
});
<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="500" height="500" style="background: black">
  <circle r="10" cx="100" cy="100" fill="red"/>
</svg>

Upvotes: 2

Kaiido
Kaiido

Reputation: 137171

The question is why

Well, you seem to know why: you never stop your animation loop, so at every frame it will try to go to the mouse.clientN position of when that animation loop started. Then at the next click, a second animation loop will start, running in parallel of the first one, and both will fight against each other to go toward their own mouse.clientN position.


To avoid this situation, as you have identified, you can simply stop the previous loop by using cancelAnimationFrame. All it takes is a variable accessible both to the animation scope and to the click handler.
However, keeping your animation loop going on is just killing trees. So make your code check if it has reached the target position before calling again requestAnimationFrame from inside animation.

const circle = document.querySelector("circle")
{

  let anim_id; // to be able to cancel the animation loop
  window.addEventListener("click", mouse => {
    const animation = _ => {

      const getCx = Number(circle.getAttribute('cx'))
      const getCy = Number(circle.getAttribute('cy'))
      const setCx = getCx + (mouse.clientX - getCx)/10;
      const setCy = getCy + (mouse.clientY - getCy)/10;

      circle.setAttribute("cx", setCx);
      circle.setAttribute("cy", setCy);
      // only if we didn't reach the target
      if(
        Math.floor( setCx ) !== mouse.x && 
        Math.floor( setCy ) !== mouse.y
      ) {
        // continue this loop
        anim_id = requestAnimationFrame(animation);
      }
    }
    // clear any previous animation loop
    cancelAnimationFrame( anim_id );
    anim_id = requestAnimationFrame(animation)
  });
  
}
svg { border: 1px solid }
<svg viewBox="0 0 500 500" width="500" height="500">
  <circle r="10" cx="100" cy="100" />
</svg>

Also, beware that your animation will run twice faster on devices with a 120Hz monitor than the ones with a 60Hz monitor, and even faster on a 240Hz. To avoid that, use a delta time.

Upvotes: 1

Related Questions