I am new to requestAnimationFrame
and trying to understand how can I develop something that is done with CSS Keyframe Animation
.
To elaborate, I can do the following with CSS
let curve = document.querySelector('#curve')
let dot = document.querySelector('#dot');
let d = curve.getAttribute('d');
dot.style.setProperty('--mp', '"' d '"')
#dot {
offset-path: path(var(--mp));
animation: move 3000ms infinite alternate ease-in-out;
}
@keyframes move {
100% {
offset-distance: 100%;
}
}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720">
<g>
<circle id="dot" r="10" cy="0.5" cx="0.5" style="fill: #dd1819; "></circle>
</g>
<path id="curve" d="M 29.928125,119.32884 C 59.868756,78.85387 99.789596,30.283895 138.66049,30.104515 c 80.46172,-0.37133 90.87185,137.794285 140.7729,129.699295 40.35037,-6.54568 49.90105,-80.94994 86.53937,-78.86548 33.24073,1.89117 22.2547,70.77048 73.14399,70.77048 39.92084,0 58.65264,-56.66496 99.80211,-56.66496 49.90105,0 69.86147,80.94995 99.8021,48.56997" style="fill: none; stroke:black; stroke-width: 1;" />
</svg>
animation-direction: alternate;
makes sure it plays back and forth and animation-iteration-count: infinite;
ensures it runs in an infinite loop.
I am trying to understand if the same can be achieved in rAF
. This is what I tried so far
let u = 0;
let time = 3000;
let start = performance.now();
let curve = document.querySelector('#curve')
let totalLength = curve.getTotalLength();
let dot = document.querySelector('#dot');
const temp = [];
const move1 = () => {
let p = curve.getPointAtLength(u * totalLength);
dot.setAttribute("transform", `translate(${p.x}, ${p.y})`);
if (u < 1) {
u = (performance.now() - start) / time
temp.push(p);
requestAnimationFrame(move1)
}
};
move1();
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720">
<g>
<circle id="dot" r="10" cy="0.5" cx="0.5" style="fill: #dd1819; "></circle>
</g>
<path id="curve" d="M 29.928125,119.32884 C 59.868756,78.85387 99.789596,30.283895 138.66049,30.104515 c 80.46172,-0.37133 90.87185,137.794285 140.7729,129.699295 40.35037,-6.54568 49.90105,-80.94994 86.53937,-78.86548 33.24073,1.89117 22.2547,70.77048 73.14399,70.77048 39.92084,0 58.65264,-56.66496 99.80211,-56.66496 49.90105,0 69.86147,80.94995 99.8021,48.56997" style="fill: none; stroke:black; stroke-width: 1;" />
</svg>
The above rAF
animation is probably equivalent to
#dot {
offset-path: path(var(--mp));
animation: move 3000ms 1 forwards linear;
}
Is there any way to achieve what can be done in CSS
CodePudding user response:
A couple of things about working with requestAnimationFrame(…)
:
It's best practice to use the timestamp argument given to the callback by
requestAnimationFrame
. There is no need to callperformance.now()
inside the function. But if you do, make sure to do it before the animation, otherwise the animation will lack behind any potential frame rate drops on slower PCs and especially phones.The callback method is passed a single argument, a DOMHighResTimeStamp similar to the one returned by
performance.now()
, which indicates the current time (based on the number of milliseconds since time origin).Take the given timestamp and get from it what you need. (This is usually done using outside variables acting as a configuration.) What iteration of the animation am I in? How far progressed has this iteration?
This approach is valid but not the best: Best practice would be to just save the last time, last requestID (returned by
requestAnimationFrame(…)
) and last state (iterationWithFraction
) of the animation outside. Then compare the last time with the time now in every step and add the normalized progress to the state. Then update the animation accordingly.
Always comparing back to the very first timestampstart
can lead to very confusing errors. (Just imaginetime
is set to2000
mid animation.)If you want to end an animation, check out
cancelAnimationFrame(requestID)
.
let time = 3000;
let start = performance.now();
let curve = document.querySelector('#curve')
let totalLength = curve.getTotalLength();
let dot = document.querySelector('#dot');
let d = curve.getAttribute('d');
const step = (timestamp) => {
// get current time normalized with "time" (2s passed = 0.6666, 3s passed = 1, 4s passed = 1.33333, ...)
const iterationWithFraction = (timestamp - start) / time;
// get current iteration as an integer
const iteration = Math.floor(iterationWithFraction);
// get current moment in iteration
let fraction = iterationWithFraction % 1;
// if iteration odd: invert fraction to get the desired alternation
if (iteration % 2 == 1) fraction = 1 - fraction;
// if you want an easing effect, you can do something like this now
fraction = fraction<.5 ? 2*fraction**2 : -1 (4-2*fraction)*fraction;
// do your animation
const point = curve.getPointAtLength(fraction * totalLength);
dot.setAttribute("transform", `translate(${point.x}, ${point.y})`);
// get next AF as the last thing of the function
requestAnimationFrame(step);
};
step(performance.now());
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720">
<g>
<circle id="dot" r="10" cy="0.5" cx="0.5" style="fill: #dd1819; "></circle>
</g>
<path id="curve" d="M 29.928125,119.32884 C 59.868756,78.85387 99.789596,30.283895 138.66049,30.104515 c 80.46172,-0.37133 90.87185,137.794285 140.7729,129.699295 40.35037,-6.54568 49.90105,-80.94994 86.53937,-78.86548 33.24073,1.89117 22.2547,70.77048 73.14399,70.77048 39.92084,0 58.65264,-56.66496 99.80211,-56.66496 49.90105,0 69.86147,80.94995 99.8021,48.56997" style="fill: none; stroke:black; stroke-width: 1;" />
</svg>
CodePudding user response:
No need for CSS or JavaScript (and I do love JavaScript & Web Components)
To add easing, see:
<svg viewBox="0 0 640 720" xmlns="http://www.w3.org/2000/svg">
<path id="PATH" stroke="black" fill="none" d="m30 119c30-40 70-89 109-89 81 0 91 138 141 130 40-7 50-81 87-79 33 2 22 71 73 71 40 0 59-57 100-57 50 0 70 81 100 49" />
<circle r="10" fill="red">
<animateMotion dur="5s" keyPoints="0;1;0" keyTimes="0;.7;1"
calcMode="linear" repeatCount="indefinite">
<mpath href="#PATH" />
</animateMotion>
</circle>
</svg>