100% JavaScript Animation
In my last articles we focused on state-based animations. We specified the states of a clockwise moving box
- with JavaScript using CSS Transitions
Article: Dynamicaly chain CSS animations - with CSS Animation
Article: Clockwise box animation with @keyframes
Both approaches have in common that we let the browser interpolate our style properties between the states. For this article we will go one step further.
We will do a 100% JavaScript-based animation. Our use-case will still be a clockwise circling box.
The basic pattern
We already know how to animate with JavaScript. You can always follow this pattern:
const animateNextFrame = () => {
// change an element's style
// [...]
requestAnimationFrame(animateNextFrame)
}
requestAnimationFrame(animateNextFrame)
Basically we call a function every time the browser wants to render a frame. Inside this function we can tell the browser what he should do.
I prepared an example with a horizontally moving box for you:
Important: When you animate without CSS Animation or Transition you need to be aware that you actually are rendering frames. The amount of frames per second differs based on processor load and monitor refresh rate.
On my 165Hz desktop monitor the animation will be faster than on my 60Hz smartphone. (Hz = Hertz, unit for frequency; meaning: number of things happening per second)
Match the different Hz rates
How can we achieve that an animation looks the same on differently fast displays? The answer is quite simple: we use time.
To know “What time it is?” we need to remember when our animation starts and compute the time gone for each frame. For that propose we use the function performance.now()
from the High Resolution Time API. It returns the number of milliseconds passed since the browser loaded the website.
const startOfAnimation = performance.now()
const animateNextFrame = () => {
const timePassed = performance.now() - startOfAnimation
console.log("since the last frame", timePassed, "ms passed")
requestAnimationFrame(animateNextFrame)
}
requestAnimationFrame(animateNextFrame)
The next step is to normalize the animation to an interval of 0.0
and 1.0
. (This may sound familiar to you if you read my article about CSS Animation.)
The value 0.0
represents the beginning and 1.0
the end of an animation iteration. In our scenario an iteration is exactly one circle.
Why should I normalize to [0.0 … 1.0]?
It has several advantages:1. You can easily apply timing functions that will change the “feel of an animation”. There are a lot of timing functions waiting for you as a math expression that are designed to work with that interval.
2. You can extend or shorten the animation duration flawlessly.
3. You get a percentage value for free. It’s really easy to further process that normalized value.
4. It’s a common technique. Other developers will understand your code. Even the guys who invented CSS Animation did it that way.
To normalize the passed time to the progress of the current animation iteration (short: progress) we utilize the modulo operator %
to “step into the current iteration” and do a simple division:
Progress = (PassedTime % Duration) / Duration
const timeGone = 3500
const animationDuration = 2000
const timePassedThisIteration = timeGone % animationDuration // 1500
const progress = timePassedThisIteration / animationDuration // 0.75
Circular movement
When you are working with something circular the first thing you should come up with is trigonometric functions like sine and cosine.
These are periodic functions which is great for a continuous animation. You pass a value between 0.0
and ~6.28
(=2 times π) and you get a value between -1.0
and 1.0
.
Just look at the blue path of the cos() function. It’s correlates to our y offset for our clockwise movement. The red path of sin() correlates with our x offset.
Here is a (counterclockwise) visualization of sine and cosine in action:
For our movement we map the progress to an angle that can be handled by our sin/cos() functions. It’s just extending the value range from [0|1] to [0|2π]: angle = progress * (2 * Math.PI)
Now we just put our angle into that cos() and sin() functions. And we multiply the result with our radius. That makes our offset. You can now add this offset to a point representing the center of our animation:
const radius = 100
const centerPosition = { left: 100, top: 100 }
const angle = progress * (2 * Math.PI)const x = centerPosition.left + Math.sin(angle) * radius
const y = centerPosition.top - Math.cos(angle) * radius
I made a sketch for you to visualize the calculations:
The Result
When we mix all three things
- the basic animation pattern
- normalization of time to a progress
- using math for circular movement
we get something like this:
A super smooth clockwise animation of our box.