August 19, 2017
There is so many ways to animate things on the web today. From pure CSS animation to fancy libraries like GSAP. The Web Animations Api (short WAAPI) tries to combine the power of CSS with the flexibility of Javascript in order to allow complex animation sequences. There are big differences between the WAAPI and for example libraries like GSAP, the biggest one being that the WAAPI is going to provide native browser support without needing to load an external library.
Since I haven’t used the Api before, I just started playing around with it in this pen.. This post is gonna be about the core concepts I think are important, the resources I found helpful and learnings that I acquired. It’s not gonna be a detailed explanation of how every function of the Api works.
The most useful resource for me was the Intro Series by Dan Wilson. Furthermore I found Rachel Nabors Talk helpful to get a feeling for the Api and what it can do. Additionally this CSS-tricks article helped me get more into the differences to CSS animation and the options available. Once I had the basics down, I mostly used the official MDN Docs to dig deeper into the Api.
I made a little illustration of the constructors that work together in the Web Animations Api. Everytime we animate something via element.animate(..)
or new Animation(...)
or document.timeline.play(…)
we get an animation object, which has different functions like play() or pause()
.
KeyframeEffects
are the core parts of how our animations are generated. It’s important to realise that these effects don’t do anything if we don’t add them to a new Anmation(..)
or play them on the document.timeline.play(..)
. If we use element.animate(..)
it’s basically the same thing as creating a KeyframeEffect
and wrapping it into a new Animation(myEffect)
.
The KeyframeEffect
takes 3 arguments (the element we want to animate, the animations frames array and the timing options object). The Effect can be stored in a variable to be used in a Sequence- or GroupEffect
. The SequenceEffect
will create an effect, that plays the KeyframeEffects
after each other, whereas the GroupEffect
will play them simultaneously.
const effectOne = new KeyframeEffect(target, frames, options);
const effectTwo = new KeyframeEffect(anotherTarget, frames, options);
const sequence = new SequenceEffect([
effectOne,
effectTwo,
]);
const animation = document.timeline.play(sequence)
animation.pause();
Once we created an animation we can use it's methods like play()
, pause()
, reverse()
or cancel()
and alter it's properties like playbackRate
.
For the planets animation I had to loop over the particle elements, the radius and the background of each planet and create a SequenceEffect
out of the single KeyframeEffects
. In order to keep an overview I created an object including all the keyframes arrays I needed.
const effects = {
fadeInLeft: [
{ transform: "translate(-100%, 0%) scale(0.6)", opacity: 0 },
{ transform: "translate(0, 0) scale(1)", opacity: 1 }
],
fadeInRight: [
{ transform: "translate(100%, 0%) scale(0.6)", opacity: 0 },
{ transform: "translate(0, 0) scale(1)", opacity: 1 }
],
}
This way I could just refer to them using effects.fadeInLeft
, which I found more convenient than creating multiple variables for each effect. I did the same thing for the options objects.
const timings = {
background: {
fill: "forwards",
duration: 800,
direction: "normal",
easing: "cubic-bezier(0.2, 0, .3, 1.5)"
},
planet: {
fill: "none",
duration: 800,
direction: "normal",
easing: "cubic-bezier(.5, 0, .3, 1)"
},
...
};
The fill
property in the options object, does the same thing as the CSS animation-fill-mode. It defines whether the changes made by the animation are kept after the animations finishes or if it goes back to what it was before. For the planets animation this was important to use, because the fadeIn
animation was different to the fadeOut
animation. While the fadeIn
animation animated all the children elements of the div.planet
, the fadeOut
animation only animated the parent div.planet
changing it’s opacity
and transform
. When I wanted to animate the child elements for a second time, the parent wasn’t visible anymore, so you couldn’t see anything. Changing the fill-mode
to fill: none;
on the parent and using animation.cancel()
on the fadeIn
animation sequence of the children, basically reset all the changes and made it possible to animate everything in again.
Using ease-in-out
is fine on a lot of animations, but it’s really worth it getting to know the cubic-bezier
syntax, because it let’s you create more interesting animations. Setting the last property to a value higher than 1, gave the planet a nice little bounce on the fadeIn
. I recommend Lea Verous cubic-bezier tool to play around with it and get to know the syntax better.
easing: "cubic-bezier(0.2, 0, .3, 1.5)"
Since I wanted the planets to go either left or right, depending on which way the slider thumb was moved, I needed to create four different keyframe effects fadeInLeft
, fadeInRight
, fadeOutLeft
and fadeOutRight
. Since the labels for the slider were also animated and positioned slightly different, they got their own keyframes and options object with the matching effects.
In order to register the update I added an eventListener
to the input
, which I debounced with a lodash debounce
debounce in order to get rid of rapidly fired events in between.
range.addEventListener(
"input",
_.debounce(e => updateSlider(e.target.value), 300)
);
To find out if the planets needed to go left or right, the previous slider index was saved in a variable and on every update compared to the current index. If it was bigger the direction was set to 1
and the document.timeline.play(fadeInLeft)
for the planet at that index was played, after the old planets document.timeline.play(fadeOutRight)
was finished. Else it was set to 0
and got animated in from the other side.
Since I didn’t want the animations to be interrupted and broken off, I created and isAnimating
variable, that’s set to false
in the beginning. When an animation is started, I set this variable to true
and once it finishes, set it to false
again. This way, when we update the slider when an animation is still active, we can call a setTimeout
and delay the update until the other animation finishes.
animation.onfinish = () => {
isAnimating = false;
};
Since I love CSS variables, I decided to use them in this pen as well, because they just make everything easier. For the planets I only had to update the --planet-color
and --particle-color
in the classes instead of reassigning all the background
properties for the children.
.planet {
--planet-color: #ffd260;
}
.planet--1 {
--planet-color: #3FB4C0;
--particle-color: #FEDB82;
}
.background {
background: var(--planet-color);
}
.particle {
background: var(--particle-color);
}
Since I wanted the thumb to update to the planet color once the slider changed, I also defined a —planet-color
variable on the thumb. Once an updated happened I set this variable to the matching color via Javascript.
range.style.setProperty(`--planet-color`, planetColors[index]);
I could have defined the —planet-color
on the :root
level, but since I already set the planets colors in the CSS and only want to update the thumb color, I only set it on the input directly, since this is more performant. If you wanna know more about CSS Variables performance you can read this article I wrote some time ago.
I love using GSAP for doing sequenced animations, just because it’s so easy, supports all the browsers, works great with SVG and can do so many amazing things. After diving into the Api it became clear, that WAAPI isn't going to replace GSAP, just because GSAP is aimed to do a lot more than the WAAPI. In order to take advantage of the Api one still has to have a good knowledge of how CSS animations works, otherwise it will be hard to use. Nevertheless I think it’s amazing to get more native animation options on the browser level without needing to load a whole library like GSAP.