Firefox has secretly implemented an AnimationTimeline to provide a timeline for animations. According to the documentation, it is an abstract class that is inherited by the DocumentTimeline.

Due to its non-standard nature, the DOCUMENTATION of MDN does not explain it clearly. It only says that it is used to allow multiple animations to share the timeline, but there is no detailed explanation of how to use it.

In this post today, I don’t want to explain how the Timeline implemented in Firefox works, but rather to extend the concept and implement a new Timeline library. Let’s see what we can do if we design a Timeline for animation or other time-dependent behavior.

Here, to illustrate the relationship between animation and Timeline, I will first show you an intuitive example:

Example 1: Timeline and animation

I have multiple animations playing simultaneously in a scene. What if I want to pause all the animations now?

If we take all the animation instances and pause them one by one, that’s fine, but inconvenient. What if I still support fast forward, slow forward? It’s gonna be messy to deal with anyway. This is where our Timeline comes into play.

Timeline can be imagined as a Timeline in the virtual world. We divide the world into many parallel universes superimposed on each other, and each universe has its own independent Timeline. All behaviors in a universe are based on the Timeline of the current universe.

For the above animations, they share an independent timeline. When we need to change the animation speed, we can directly change the playbackRate of timeline to control the time passing speed.

How?

Some simpler examples:

Let’s start with a simple circular motion animation that doesn’t use Timeline:

Example 2 – Do not use Timeline

let startTime = Date.now(), T = 2000

requestAnimationFrame(function update(){
  let p = (Date.now() - startTime) / T

  ball.style.transform = `rotate(${360 * p}deg)`
  requestAnimationFrame(update)
})


Copy the code

The above example is very simple. Calculate the Angle of rotation of the ball and animate it in a circle. But what if we wanted to animate the ball twice as fast or half as fast without changing its motion parameters? We think of the ball movement as a movie, and we want to change the speed of the player without changing the actual time in the movie. At this point we need to introduce the timeline:

Example 3 – Primary speed

let timeline = new Timeline()
let startTime = timeline.currentTime, T = 2000

requestAnimationFrame(function update(){
  let p = (timeline.currentTime - startTime) / T

  ball.style.transform = `rotate(${360 * p}deg)`
  requestAnimationFrame(update)
})


Copy the code

Example 3 above is very similar to example 2 above, except that we have replaced date.now () with timeline.currentTime, which is our timeline instead of the system default time. Once we’ve done that, we can speed up or slow down the animation by adjusting the timeline parameter playbackRate!

Example 4-2 times the speed

let timeline = new Timeline({playbackRate: 2.0}) let startTime = timeline. CurrentTime, T = 2000 requestAnimationFrame(function update(){ let p = (timeline.currentTime - startTime) / T ball.style.transform = `rotate(${360 * p}deg)` requestAnimationFrame(update) })Copy the code

Example 5-1/2 velocity

let timeline = new Timeline({playbackRate: .5})
let startTime = timeline.currentTime, T = 2000

requestAnimationFrame(function update(){
  let p = (timeline.currentTime - startTime) / T

  ball.style.transform = `rotate(${360 * p}deg)`
  requestAnimationFrame(update)
})


Copy the code

Example 6-2 times speed in reverse

let timeline = new Timeline({playbackRate: -2.0}) let startTime = timeline. CurrentTime, T = 2000 requestAnimationFrame(function update(){ let p = (timeline.currentTime - startTime) / T ball.style.transform = `rotate(${360 * p}deg)` requestAnimationFrame(update) })Copy the code

Time axis and Timer

As you can see from the above example, all Timeline does is independently calculate currentTime based on the playbackRate, so all we need to get the time is just use timeline.currentTime instead of date.now (). However, for ease of use, our Timeline also provides its own timer:

Example 7 – millisecond to second

let timeline = new Timeline({playbackRate: 0.001}) timeline. SetInterval (() => {ball.innerhtml = math.round (timeline. CurrentTime)}, 1)Copy the code

Timeline provides four methods, setInterval, setTimeout, clearInterval, and clearTimeout, respectively corresponding to the four corresponding methods of window. However, the time elapsed is based on the playbackRate of timeline.

CurrentTime with entropy

Because Timeline’s playbackRate is dynamic, its currentTime is also dynamic, which will affect its timer, for example:

Example 8 – Turning back time?

let timeline = new Timeline({originTime: -100, playbackRate: -0.001}) timeline.setInterval(() => {ball.innerhtml = math.round (timeline.currentTime)}, 1)Copy the code

In this example, we rewind the clock and the number decreases every second, which seems fine, but look at it another way:

Example 9 – The time warp bug

Let timeline = new timeline ({originTime: -100, playbackRate: -0.001}) let count = 100; timeline.setInterval(() => { ball.innerHTML = count-- }, 1)Copy the code

We found that the timer didn’t actually run every second as we expected. This is because we changed the direction of the arrow of time by setting the playbackRate to negative. This means that history and the future are reversed, so the setInterval does not fire after 1 second, but immediately, because “future” is a negative time for the timer, and “1 second after” is already past!

Let’s make a change:

Example 10 – negative timer

Let timeline = new timeline ({originTime: -100, playbackRate: -0.001}) let count = 100; timeline.setInterval(() => { ball.innerHTML = count-- }, -1)Copy the code

So if the playbackRate is negative, then the timer should also be negative. This is cumbersome and error-prone. And sometimes we can’t guarantee that the timer will fire, for example if we change the direction of the playbackRate periodically, it’s possible to limit the time to a range and the timer will never fire.

Sometimes we need to explicitly trigger the timer after the timeline has waited a certain amount of time, regardless of whether the time arrow is forward or backward, so we can use the entropy property.

Entropy means entropy, and regardless of whether the playbackRate is positive or negative, entropy can only increase, not decrease. But entropy is also affected by the playbackRate. That is, entropy is only relevant to the absolute value of the playbackRate, not its sign.

So we could also write:

Example 11 – Entropy and timer

Let timeline = new timeline ({originTime: -100, playbackRate: -0.001}) let count = 100; timeline.setInterval(() => { ball.innerHTML = count-- }, {entropy: 1})Copy the code

Entropy is useful when changing the playbackRate dynamically in a scene. It provides a one-way measure of time that allows us to control the speed and flow of the animation, for example:

Example 12 – Entropy control animation

const T = 2000 let timeline = new Timeline() timeline.setInterval(function update() { ball.innerHTML = Math.round(timeline.currentTime / 100) if(timeline.playbackRate < 0){ ball.style.backgroundColor = 'green' } else { ball.style.backgroundColor = 'red' } }, {entropy: 100}) speedup.onclick = function(){if(timeline) timeline.playbackRate += 0.2rate.innerhtml = Timeline. PlaybackRate. ToFixed (1)} slowDown. The onclick = function () {if (timeline) timeline. PlaybackRate - = 0.2 rate.innerHTML = timeline.playbackRate.toFixed(1) } reverse.onclick = function(){ if(timeline) timeline.playbackRate = -timeline.playbackRate rate.innerHTML = timeline.playbackRate.toFixed(1) } pause.onclick = function(){ if(timeline) timeline.playbackRate = 0 rate.innerHTML = timeline.playbackRate.toFixed(1) }Copy the code

Timeline inheritance — fork

Interestingly, we can also create a new timeline relative to the current one based on the current one. This way, we can influence all child timelines forking out by controlling the parent timeline as well as the individual timeline, which provides great flexibility.

Example 13 – Timelien fork

let timeline = new Timeline()

function count(el, timeline, p = Infinity) {
  timeline.setInterval(() => {
    el.innerHTML = Math.round(timeline.currentTime / 1000) % p
  },  {entropy: 1000})
}

count(ball0, timeline)
count(ball1, timeline.fork({playbackRate: 10}), 10)
count(ball2, timeline.fork({playbackRate: 100}), 10)


Copy the code

conclusion

Timeline is a secondary class that greatly enhances animation control by controlling the time flow and direction of the animation to change the progress of the animation. To use the powerful Timeline, download it from the GitHub repo.

Any questions, welcome to discuss ~~