Animating CSS Gradients, One Frame at a Time
Posted By Neale Van Fleet on October 10th, 2018
While working on the recent launch of Airfoil 5.8 (with Sonos support!), I wanted to visually convey the idea of wireless broadcasting by using the tried-and-true pattern of concentric circles. To really sell the effect, I wanted these circles to animate when the mouse cursor was hovering over them. This took some doing, but a bit of inspiration from my childhood got us where we wanted to be.
Getting Started with Gradients
The easiest way to display concentric rings is a simple radial gradient, done in CSS. CSS gradients are a lightweight way to add all sorts of visual patterns to the web. This means not just the smooth gradual changes between colours generally associated with the word “gradient”, but also lots of other repeating patterns.
This is the desired look, but I really wanted these radial circles to move.
Gradients are super useful, but they’re usually about as animated as a Moai. Unlike many other web elements, you can’t just set the start and end states, then have the browser interpolate out a smooth animation. I expect this will one day work in the major browsers, but for the moment it’s not supported.1
Stop Motion Inspiration
To get the animation I was after, I reached back into my childhood. Long ago, in the days of VHS, I made stop-motion animations with my family’s clunky camcorder. I would painstakingly build a tiny set and characters out of clay, then move it all very slightly, to animate a scene one fraction of a second at a time. It was arduous, but much like CSS now, I had to work with what I had.
A still frame of an unlucky Play-Doh guy getting hit in the head with a snowball, circa 1992 or so. Sorry Pixar, I went into software design instead of animation.
I realized we could put a new twist on that old stop-motion technique, by writing out CSS to create one frame at a time, then using the animate function to cycle through them. It would work a lot like those old animations I used to make, but with the “frames” created from code instead of images.
Animating with CSS
Doing this in CSS requires two steps. We first defined our frames using the @keyframes function. After that, we applied the animation to our banner with the Animate function, which defines how the frames will play.
Step 1: Defining The Necessary Frames
The good thing is that the CSS needed to generate each frame of our animation was relatively straightforward, though quite repetitive. We simply incremented the progress count by 2%, and the radius of the ring portion of the gradient by 1px each frame. My co-worker Lee made a quick script to brute-force generate the 51 frames in our loop, which we then placed in the related CSS file.2
I won’t show all the frames here, as there are too many, but here are the first two frames as an example. One thing worth noting is that we actually built the animation with the rings radiating outward, because the math was slightly easier. In the second step, that animation is reversed, to get the desired effect.
Here’s the first (0%) frame of an animation I’m naming waves:
@-webkit-keyframes waves {
0% {
background: repeating-radial-gradient( 15% 50%, circle, #E5F8FF, #E5F8FF 0px, #B8DDED 0px, #B8DDED 2px, #E5F8FF 0px, #E5F8FF 52px);
}
This code may be a little complicated to the uninitiated. It sets the repeating background gradient we want, with the size of the inner ring at 0px, and a total width of 52px. We need to increment the radius of the inner ring by 1px for each frame, to make the rings move a single pixel outward (remember, we’ll reverse it later). So, the second frame looked like this:
2% {
background: repeating-radial-gradient( 15% 50%, circle, #E5F8FF, #E5F8FF 1px, #B8DDED 1px, #B8DDED 3px, #E5F8FF 0px, #E5F8FF 52px);
}
There are another 49 frames, omitted here in the name of brevity, which increment the ring size by one pixel per frame until we hit 100%.
Step 2: Making Those Frames Move
The above code creates the individual frames which will result in our desired animation. However, we still have to define how they will animate. HTML and CSS keeps the animation and the definition of how it plays separate in the name of reusability. Making this animation apply to our banner was then as simple as adding the following bit of CSS of our banner element:
.banner:hover {
animation-name: waves;
animation-iteration-count: infinite;
animation-duration: 1s;
animation-direction: reverse;
}
This last bit of code is equivalent to the playback controls, and defines aspects such as speed, iteration count, and direction. By separating the actual animation elements (the frames and instructions), from the playback controls, you could very easily apply the same animation to different elements, but with different playback parameters.
A good example of how this separation is useful is that our animation frames are built in the opposite direction of how we wanted them to run, so we’ve specified in the playback controls for them to run in reverse. If I wanted it to run the other way, or even ping-pong back and forth, I wouldn’t have to define a whole new animation, I’d just edit these playback controls.
Our End Result
Here’s a slightly modified version of the banner which appeared on our front page. To better show the effect, this version doesn’t require hover to animate:
As you can see, our CSS animation got us what we wanted. The concentric rings draw via CSS, and are then animated inward, representing audio streaming to these devices. CSS gradients and animation allowed me to achieve the effect I wanted, while remaining very lightweight. The frames are only about 15KB in total, a pittance for an animation in today’s world of overweight web pages.
I’m very happy with this result. This was an effect I’ve wanted before, but I hadn’t previously figured out the right approach. There’s a good chance you’ll see this technique used again before too long on our site. Perhaps you can make use of it yourself as well!
Footnotes:
-
Surprise kudos to Microsoft, because the current version of Edge does support this type of animation. ↩︎
-
60 frames would have been ideal, to fit into the 60 frames per second most computers display. However, CSS keyframes are defined as percentages, which makes it difficult to define keyframes that don’t fit easily into 100. A bit more math may have made it possible to get the animation to take exactly 60 frames, but 51 frames wound up working quite well, with less work. ↩︎