In a recent article, I described the mathematics underlying my animated artwork The Passing of Time.
My article contained a demo that showed a simplified implementation of the core concepts. This demo contained an animation loop of the form:
function animate () {
window.requestAnimationFrame(animate)
// render scene, dependent on the value of `time`
...
time += 1 // increment time
}
This code has a problem. The problem is that the speed at which the animation runs depends on the frame rate at which the browser requests new animation frames. On many modern computers, operating systems, and browsers, this will be 60 frames per second (FPS), but on very powerful machines 120 FPS is also common, and thus the animation would run twice as fast on those machines.
To make an animated piece behave the same across the largest range of different hardware and OS options, we therefore have to control the speed at which the animation proceeds irrespective of the FPS that the computer tries to achieve. There are two different ways in which we can implement this control, and I'd like to explain both here. The first one drops frames whenever the browser requests frames more quickly than the target frame rate. The second one instead draws every requested frame but keeps track of the exact amount of time that has passed between successive frames and updates the animation accordingly.
Regardless of the method chosen, we need a few variables to keep track of core parameters needed for animation speed control:
// animation speed control
let last_timestamp = 0,
target_FPS = 60,
anim_timestep = 1000 / target_FPS // ms per each frame
Here, last_timestamp
is used to store the time point at which the last animation frame was requested, target_FPS
is the desired rate of frames per second, and anim_timestep
is the time in milliseconds that passes between successive frames at the desired frame rate.
Now let's consider the first technique for controlling animation speed, dropping frames. To determine whether we should draw a frame or drop it, we need to know how much time has passed since the last frame that we drew. Fortunately, this is made easy by the fact that the browser can supply a time stamp representing the current time in milliseconds. We just need to give the animate()
function an argument that can accept this time stamp. We then calculate the difference between the current time stamp and the last time stamp, and, if this distance is too small, abort the drawing function. Otherwise we increment the time variable, record the time stamp, and draw.
function animate (timestamp) {
window.requestAnimationFrame(animate)
let delta = timestamp - last_timestamp
// check if sufficient time has past to draw a new frame
if (delta < anim_timestep) {
return
}
time += 1
// record last timestamp
last_timestamp = timestamp
// render scene, dependent on the value of `time`
...
}
This is the mechanism that I used in The Passing of Time, and in general it works well. However, it has some potential problems, such as not being able to adapt to situations where the browser requests frames too slowly rather than too fast.
In hindsight there's a simpler way to control animation speed: Instead of incrementing the time variable by 1 every time we draw an animation frame, we instead increment by the ratio of the time elapsed and the targeted wait time between frames. If frames are requested at exactly the right speed then this ratio is 1 and we get the same result as before. But, if instead frames are requested, for example, at twice the speed then delta / anim_timestep
will be a value near 0.5, and so now the time variable gets incremented by 1 every two frames, resulting in the same perceived animation speed.
function animate (timestamp) {
window.requestAnimationFrame(animate)
let delta = timestamp - last_timestamp
time += delta / anim_timestep
// record last timestamp
last_timestamp = timestamp
// render scene, dependent on the value of `time`
...
}
I like animations that can be started and stopped, but this adds the complication that time is passing while the animation is stopped but we want to ignore this elapsed time when we restart the animation again. The simple solution to this problem is to ignore the amount of time that has passed when it is too large, for example larger than 500 milliseconds:
time += delta > 500 ? 1 : delta / anim_timestep
Between these two methods, dropping frames or adjusting time increments, the second one is clearly better. It results in smoother animations, by never dropping frames and by drawing frames as fast as the browser can handle. Going forward, I expect that I'll always use the second method in all animations that I write.
A computationally generated color palette
Let me also quickly explain how color palettes are generated in The Passing of Time. All color palettes are generated computationally, using simple trigonometric formulas. The idea goes back to an article by Inigo Quilez, who suggests to express the red (R), green (G), and blue (B) intensity of each color using a simple trigonometric formula of the form:
Here, I(t) is the color intensity (R, G, or B) as a function of a parameter t that runs from 0 to 1, and a, b, c, and d are parameters that in the most general case are chosen independently for each color component R, G, or B. Thus, in the most general case each palette is specified by twelve independent parameters.
Now let's talk about how to implement this in JavaScript. My goal is to have a palette function that takes a parameter t as input and returns an RGB hex code representing the color. To implement such a function, I first need a function that can take three numeric values R, G, and B and return the corresponding hex code. This code looks as follows:
// convert a numeric value between 0 and 255 into two-digit hex code
const toHex = (x) => {
x = Math.floor(x + 0.5).toString(16)
return x.length === 1 ? "0" + x : x
}
// create color hex code from numerical R, G, and B components
const rgb = (R, G, B) => `#${toHex(R)}${toHex(G)}${toHex(B)}`
Now I can implement the actual palette function. Here, I'll just use fixed constants to generate one specific palette. The actual long-form artwork generates some of the constants randomly from a probability distribution. For simplicity, I'm using the same constants a, b, and c for all three color components and only vary the constant d among the components. My code looks as follows:
const palette = (x) => rgb(
255 * (0.5 + 0.4 * Math.sin(6.2832 * x)),
255 * (0.5 + 0.4 * Math.sin(6.2832 * (x - 0.06))),
255 * (0.5 + 0.4 * Math.sin(6.2832 * (x + 0.17)))
)
Let's add one last piece: We can make the color palette a little more interesting if we introduce some noise, so that the color gradients generated by the palette function have some additional variation and structure. The simplest way to do this is to add some small random number to each color intensity that we generate.
We could generate these random numbers with some random number generator, but here I'll use an even simpler approach that is frequently employed in shader programming. I multiply the input variable x
with some large number, then take the sin of that number, and then extract the fractional part. This results in a somewhat random mapping from a numerical input variable to a number between 0 and 1, and it's good enough for demonstration purposes.
const hash = (x) => Math.sin(59482.23 * x) % 1
The palette function then becomes:
const palette = (x) => rgb(
255 * (0.5 + 0.1 * hash(x) + 0.4 * Math.sin(6.2832 * x)),
255 * (0.5 + 0.1 * hash(x) + 0.4 * Math.sin(6.2832 * (x - 0.06))),
255 * (0.5 + 0.1 * hash(x) + 0.4 * Math.sin(6.2832 * (x + 0.17)))
)
And with that little trick, we generate outputs that look like this:
You can see a live version of this Alien Clock demo here. And you can find the complete source code of this latest and all prior Alien Clock demos on GitHub. All code has been released into the public domain and you are free to use any of these concepts and ideas in your own work.