This is an explainer for animation.overallProgress
, a proposal to add an "overallProgress"
property to the JavaScript class
Animation.
The goal of this property is to provide authors a convenient and consistent representation of how far along an animation has advanced across its iterations and regardless of the nature of its timeline.
There isn't a method or property which authors can use to directly know how much an animation has advanced through its duration in a way that:
- accounts for all of its iterations,
- offers a consistent representation for both scroll-driven and time-driven animations.
There does exist an AnimationEffect.getComputedTiming().progress
API but it only reflects the progress of the current iteration of the animation.
Add a read-only "overallProgress" field to Animation which is represented the same way for both time-driven and scroll-driven animations and accounts for the state of the animation and the actual time/scroll range during which the animation is active.
The proposed read-only overallProgress
accessor allows authors to update other parts
of their page based on how far along an animation has advanced. They can use
this to:
- Synchronize another element's appearance, e.g. a video, according to the
animation's
overllProgress
as in the scroll-driven example below. - Synchronize audio on a page with the progress of the animation as in the time-driven example below.
- Give the user a sense of when an ongoing visual effect will end.
In this demo
the StereoPannerNode's
pan value
is adjusted based on the progress of the time-driven animation moving the car
across the screen over several iterations. The audio is panned from left to right as the animation makes progress from the leftmost scene to the rightmost one. In this example, the author
starts an animation when the button is clicks and performs updates based on the
progress of the animation via requestAnimationFrame
calls:
...
function progress() {
return animation.overallProgress;
}
document.querySelector('#startbutton').addEventListener('click', (e) => {
...
animation.play();
const update = () => {
adjustAudio(progress());
...
};
requestAnimationFrame(update);
startAudio();
});
...
In this demo the overall progress of a scroll-driven animation is used to set the video's current frame.
Here is a scroll-driven demo where the developer synchronizes a textual indication of how far into a section of the text the user has scrolled with a graphical indication (the green bar).
To do this, they create an animation using a scroll-timeline:
const makeAnimation = (subject, rangeStart, rangeEnd) => {
const viewtimeline = new ViewTimeline(
{
subject: subject,
axis: 'y'
}
);
const animation = meterbar.animate(
[
{ height: "0%" },
{ height: "100%" },
],
{
timeline: viewtimeline,
rangeStart: rangeStart,
rangeEnd: rangeEnd,
fill: "forwards"
});
return animation;
}
const animation = makeAnimation(target, "cover 0%", "contain 100%");
and observe, during every scroll event, what the overallProgress
of the animation is.
Since this animation is driven by scrolling, the developer could get the overall progress by doing a few calculations based on scrollTop:
function progress() {
const lower_bound = (target.offsetTop - scroller.offsetTop) - scroller.clientHeight;
const upper_bound = lower_bound + target.offsetHeight;
// Compute raw fraction.
let progress = (scroller.scrollTop - lower_bound) / (upper_bound - lower_bound);
// Clamp to [0, 1];
progress = Math.min(progress, 1);
progress = Math.max(progress, 0);
return progress;
}
which is slightly more tedious than what they could do with animation.overallProgress
:
function progress() {
return animation.overallProgress;
}
They'd then update the textual indication on every scroll event.
const updateViewInfo = () => {
...
const progress_pct = progress() * 100;
viewInfo.innerHTML = `<h3>Required Section progress: ${ Math.round(progress_pct) }%.</h3>`;
...
};
scroller.addEventListener("scroll", updateViewInfo);
Note that the above scrollTop
calculation is what corresponds to the
rangeStart
and rangeEnd
of "cover 0%" and "contain 100%" respectively
(in makeAnimation()
) in this particular layout. It might need to
be adjusted slightly to correspond to a different scroll range and layout.
Animation.overallProgress
is generally defined as
progress = currentTime / effect endTime
where currentTime
is the currentTime
of the animation and effect endTime
is the endTime of the animation's effect.
It is however clamped to values in the range [0, 1] and will be null if:
- the animation's currentTime is null, or
- the animation has no effect.
If an animation's endTime
is zero, its overallProgress
will be:
- 0 if its currentTime is negative, and
- 1, otherwise.
If an animation has infinite endTime its progress is 0.
By clamping animation.overallProgress
to [0,1] we handle the case of zero-duration
animations by returning values of 0 or 1 which are more likely to be useful to
developers than the more mathematically correct -Infinity
and +Infinity
.
This however means that an animation A
whose currentTime
is
less than its startTime
and a non-zero-duration animation B
whose
currentTime
is equal to its startTime
(and may therefore be visually
reflecting the animation's having started) both report overallProgress
of zero.