Frame VS Time Animation

Frame vs Time-based Animations

Making an animation for a client

Let's say that you're working on an animation for a client. They wanted an animated canvas that loops. So far, you have this:

The trouble is, the client just informed you that this animation might be running on older hardware, so it has to run at 30 frames per second. Fortunately, you can just use the frameRate() function in the setup loop to fix that:

frameRate(30)

Oh no, it's too slow now!

You changed the framerate from 60 to 30, but now the animation plays half as fast. Try it yourself — add frameRate(30) to the setup function above and see what happens.

This is because the animation is frame-dependent. The speed of change is directly proportional to the framerate — t = frameCount increments once per frame, so at 30fps it only increments 30 times per second instead of 60. To fix this, we need to measure the rate of change in time rather than frames.

Summary

In short, in order to make the animation frame-independent, we need to:

  • track what time it is each frame
  • compare the change in time between frames
  • use the change in time to influence the theta

Timeline Class

Okay, let's set up a class that does all of the tasks we just outlined. This tutorial is going to name the class Timeline since more animation functionality will be added to it in future tutorials.

class Timeline {}

Since the object only needs to compare two points in time (the current frame and the previous frame), it'll need to store those two values.

class Timeline {
  constructor() {
    this.now
    this.then
  }
}

Getting the current time

p5 has a built-in function for getting the current time: millis(). It returns the number of milliseconds that have elapsed since the sketch started.

Why not Date.now()? JavaScript also has Date.now(), which returns the number of milliseconds since January 1st, 1970 — a much larger number. Both would work fine for delta time (since we're only ever looking at the difference between two calls), but millis() is simpler because it starts at zero when your sketch does. It's also already part of p5, so there's no need to reach outside the library.

Why January 1st, 1970? This date is called the Unix epoch — the arbitrary zero point that UNIX-based operating systems use as their reference for all timestamps. It was chosen by the engineers who designed UNIX in the late 1960s, mostly for practical reasons: they needed a recent date that fit cleanly into a 32-bit integer and wouldn't overflow for decades. Storing time as "milliseconds since a fixed point" also makes it trivially easy to compare two timestamps or calculate durations — just subtract.

Updating then and now

Cool, so now that you know what function will get you the time, it's time to put it to use! Start by adding it to your constructor, then make an update function to reassign the values.

Since both now and then need to be initialized before the first update() call, set them both to millis() in the constructor:

class Timeline {
  constructor() {
    this.now = millis()
    this.then = millis()
  }
  update() {
    this.then = this.now
    this.now = millis()
  }
}

Delta Time

Great! Now that there's a then and now being recorded, the difference between them (the delta) can be found. All you have to do is make a function that subtracts then from now:

deltaTime() {
  return this.now - this.then
}

Now you're ready to start integrating this with the rest of the animation!

Integrating the Timeline

Now let's add a Timeline object to the animation and replace the frameCount logic with deltaTime().

But now the animation is way too fast! That's because when frameCount * 0.025 was being used, t was increasing by about 0.025 each frame. Now that deltaTime() is being used, it's increasing by about 32 each frame — because 32 milliseconds have elapsed since the last frame.

Returning seconds instead of milliseconds

The solution is to convert the deltaTime to seconds by dividing by 1000. You could divide each call to deltaTime(), but it'll save time in the long run to add that division inside the Timeline method itself:

deltaTime() {
  return (this.now - this.then) * 0.001
}

From there, you can scale the speed of the animation by multiplying deltaTime() before adding it to t. The right multiplier is a matter of aesthetics and personal preference.

t += mTime.deltaTime() * 50

Great! Now the animation runs at the same speed regardless of its framerate. Try changing the frameRate() value in the setup loop — the animation should look identical whether you use 15, 30, or 60.