Reputation: 33
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
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
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