Skip to content

Instantly share code, notes, and snippets.

@e111077
Last active April 19, 2020 02:25
Show Gist options
  • Save e111077/17720b595707241e1ed361852a03dc23 to your computer and use it in GitHub Desktop.
Save e111077/17720b595707241e1ed361852a03dc23 to your computer and use it in GitHub Desktop.

SpringCleaning.playAnimation()

You are probably here because you've probably seen this message on neon-animation.

⚠️ This element is now deprecated ⚠️

and to you this may seem like

🔥 This element is now deprecated 🔥

Well, rest easy, because nothing is changing. By deprecated we mean that we will keep the element as-is. If it works for you, it should keep working for you. There won't be any major changes coming to it.

Why is this happening?

Several reasons:

  • The implementation of the neon-animation elements are more-complicated than they need to be, and it can be hard to get performant animations. Making performant animations is hard enough as it is.
  • The neon-animation system requires you to use a proprietary JSON configuration object instead of the native animation interfaces (CSS keyframe animations, CSS transitions, and the web animations API). We on the Polymer team found it easier to implement new animations using the native APIs rather than neon-animation—and that seemed like a bad sign.

For these reasons, we're not planning on maintaining the package going forward.

What should I do?

If you're creating a new element from scratch, we recommend using CSS keyframe animations, CSS transitions, or the web animations API.

If you're using another element from the PolymerElements org that depends on neon-animation, like iron-dropdown or paper-menu-button, don't sweat it. We'll replace these animations. If you're using neon-animation directly, you should know that we're not going to maintain it going forward (Windows XP style). That doesn't mean it's going away overnight, and if it's working for you now, it should probably keep working.

We'd be open to community maintainers taking over this pacakge if there's interest, but for the reasons mentioned above, there are drawbacks.

Alternatives

As I've mentioned, animations have not gone away, they've just moved to the platform. There are currently 3 different alternatives that we suggest (in no particular order):

  1. CSS Keyframe Animations
  2. Web Animations API
  3. CSS Transitions

CSS Keyframe Animations

CSS keyframes are probably one of the best substitutes for our static animations. This is because they are native, simple to write, and pretty well supported cross-browser.

The basics of CSS Keyframe animations are pretty well-covered by Mozilla.

TL;DR Migration

The fastest and simplest way to migrate a neon-animation to a CSS keyframe animation is the follwing:

  1. Find the animation you want to use in neon-animation/animations
  2. Create an @keyframe rule in a style tag (can be html imported or just in the shadow dom of the element) which can be generated from the arguments of the KeyframeEffect class in the config function.
  3. Create a CSS rule that will apply the animation, and configure (e.g. timing, repetitions, etc.) the element(s) that you wish to animate.
  4. Create a playAnimation function in the animatable element
    • This function should match the elements that you want to animate to the animatable CSS rule (e.g. add an animating class and make .animating play the animation).
    • This function should also run the logic in the configure function of the animation. If it is a dynamic property, then it may also have to edit the @keyframe rules.
  5. Add an animationend event listener in the ready lifecycle callback to the elements that will animate. This listener should make the now-finished animated element no-longer match the animatable CSS rule.

👍s and 👎s

CSS Keyframe animations are great beacuse they provide great control over animations because they introduce the concept of a Keyframe which essentially represent a CSS snapshot at a specific percentage of an animation's timeline. They have also been supported since (prefixed) Obama's first term (unprefixed around when The Weeknd stopped feeling their face).

What makes them not so great is trying to make changes to the keyframes. To make changes, you have to first locate the CSSStyleSheet and within that CSSStyleSheet you must find the correct CSSKeyframesRule and then make your edits. It's possible but it's ugly, painful, and not always efficient.

Web Animations API

The Web Animations API is a great all-around solution for neon-animation as neon-animation was designed around the Web Animations API. The main drawback is that the API is currently not well-supported across browsers, though there is a polyfill available.

Again, Mozilla has a pretty good intro to the API, and we will be focusing on the element.animate and the Animation APIs.

TL;DR Migration

This process is very similar to the CSS Keyframe animations:

  1. Find the animation you want to use in neon-animation/animations
  2. Create a playAnimation function in the animatable element
    • This function should call animate on the elements that need to be animated with the keyframes generated from the corresponding configure function of the animation.
  3. Add an onfinish callback to Animation object returned by. This callback should reset the state of the element so that it can be animated again.

👍s and 👎s

The Web Animations API is great because it does everything the CSS Keyframe Animations do, but it also has the luxury of being in Javascript and thus can make changes on the fly. Though it is not all roses; the Web Animations API is not natively supported by most browsers and may never be.

CSS Transitions

CSS Transitions are the traditional way to animate. Though, transitions take a different approach to animation management than the other two methods discussed here. Instead of animating based on states, it animates based on changes (i.e. on transition 😜).

Here's a trusty MDN link on how to use them.

TL;DR Migration

Since CSS Transitions have a different interaction model (on change rather than on state), there may not be a simple 1:1 migration for every type of animation and most of the migration process is dedicated to making the animation function like keyframes.

  1. Find the animation you want to use in neon-animation/animations
  2. Create a playAnimation function in the animatable element
  3. This function should set the state of the first keyframe of the animation on the elements that you want to animate. e.g. if it is supposed to fade out, the first keyframe should be opacity: 1, so this should set the opacity to 1.
  4. Define the transition of the element.
  5. Begin the transition animation by setting the second keyframe. e.g. with the fade-out animation you set opacity to 0.
  6. Step through the keyframes by listening to the transitionend event (I personally like to use a state machine with a switch statement). e.g. if you want to fade back in you'd set the opacity back to 1.
  7. Remove the listener, reset the state of the animated elements, and notify the user of this animatable webcomponent that the animation is finished.

👍s and 👎s

Transitions have been around since IE11 and are safe to use on almost any browser. They are super easy to use on single-step animations. Transitions can also easily be set via javascript so you don't run into the same issues as CSS Keyframe animations.

The negatives are that you lose the easy control over the animation. Instead of specifying what happens when, transitions must specify what happens for how long after the previous change.

Examples

We are going to look at 2 different types of animations: static keyframe animations, and dynamic keyframe animations. Animations that I consider "static" are an animation that does not need changes to the keyframes (e.g. fade in fade out just changes opacity from 0 to 1 and 1 to 0). Animations that are "dynamic" are animations that need to update keyframes between different runs of the same animation (e.g. setting a new target for a hero animation).

Static

Let's do a simple static animation; fade out and then reverse the animation to fade back in.

CSS Keyframes

CSS Keyframe animations are the💣.com when it comes to static animations. This is because you can declaratively define an animation with great control of the timing.

Here is example code to a fade-out fade-in animation implemented with css @keyframes

<iframe src="https://nimble-stop.glitch.me/css/fade.html" width="100%" style="border:solid 1px black;"></iframe>

In this basic example, we define the fadeOut animation (taken from neon-animation):

@keyframes fadeOut {
  /* initial frame */
  0% {
    opacity: 1;
  }
    
  /* end frame */
  100% {
    opacity: 0;
  }
}

We then associate this animation with the fading class, so any element that matches the .fading CSS selector will play this animation. We associate it like so:

.fading {
  /* name of predefined keyframe animation */
  animation-name: fadeOut;
  /* play animation twice */
  animation-iteration-count: 2;
  /* alternate between playing forward then backward */
  animation-direction: alternate;
  /* plays twice for a total of 1 second */
  animation-duration: .5s;
}

For quick backwards compatibility, we define a playAnimation function that adds the fading class, playing the animation. This function also adds an animating property for the sake of notifying the user of this element of the animation.

playAnimation() {
  if (!this.animating) {
    // set property for data binding (observer will call
    // playAnimation again)
    this.animating = true;
    return;
  }

  this.$.text.classList.add('fading');
}

The final piece is to reset the animation by removing the fading class by listening to the animationend event:

ready() {
  super.ready();

  // reset when animation finishes observer removes fading
  this.$.text.addEventListener('animationend',
      _ => this.animating = false);
}

Just like that, we were able to implement a fade in / fade out animation with just one reversible rule. The downside is just a lot of boilerplate.

Web Animations API

Static animations are farily straightforward with the Web Animations API. Here is example code to a fade-out fade-in animation implemented with the Web Animations API.

<iframe src="https://nimble-stop.glitch.me/wa/fade.html" width="100%" style="border:solid 1px black;"></iframe>

In this basic example we write a simple playAnimation function:

playAnimation() {
  if (!this.animating) {
    // set property for data binding (observer will call
    // playAnimation again)
    this.animating = true;
    return;
  }

  const animation = this.$.text.animate([
    {opacity: 1},
    {opacity: 0},
    {opacity: 1}
  ], {
    duration: 1000
  });
  
  animation.onfinish = _ => {
    this.animating = false;
  };
}

Here, we simply call the animate function, pass in the keyframes, and reset the state via the onfinish callback. This method is fairly straightforward and has a lot less boilerplate than the CSS Keyframes.

CSS Transitions

Static animations with transitions are generally easy, but the complexity comes when you are trying to have a step-by-step keyframe structure. Here is example code to a fade-out fade-in animation implemented with CSS Transitions.

<iframe src="https://nimble-stop.glitch.me/ttn/fade.html" width="100%" style="border:solid 1px black;"></iframe>

Here is the playAnimation function for this demo:

playAnimation() {
  if (!this.animating) {
    // set property for data binding (observer will call
    // playAnimation again)
    this.animating = true;
    return;
  }
  
  // initial keyframe (visible)
  this.$.text.style.opacity = 1;
  this.$.text.style.transition = 'opacity ease-out .5s';
  
  // "keyframe"-like state machine
  const transitionListener = _ => {
    switch (this.__animationStep) {
      // third keyframe (fade back in)
      case 0:
        this.$.text.style.transition = 'opacity ease-in .5s';
        this.$.text.style.opacity = 1;
        this.__animationStep += 1;
        break;
      // reset state to before animation
      case 1:
        this.$.text.removeEventListener('transitionend', transitionListener);
        this.$.text.style.transition = null;
        this.$.text.style.opacity = null;
        this.animating = false;
        this.__animationStep = 0;
        break;
    }
  }
  
  this.$.text.addEventListener('transitionend', transitionListener);
  // second keyframe (fade out)
  this.$.text.style.opacity = 0;
}

Here, we set the initial state on the element, define the state machine that functions as a keyframe-like animation controller, begin listening to transition changes, and then initiate the animation by triggering the transition.

Dynamic

For a dynamic animation, we are going to implement the hero animation. The hero animation is dynamic because on each click, the animation needs to calculate which box to transform and calculate the scale and translation across the screen from the bounding boxes of the origin and the destination.

CSS Keyframes

When it comes to animations that require Keyframe changes (like the hero animation), CSS Keyframes are not necessarily the best option. This is generally because editing CSS rules in style tags is cumbersome.

Here is example code to a hero animation implemented with css @keyframes

<iframe src="https://nimble-stop.glitch.me/css/hero.html" width="100%" height="300px" style="border:solid 1px black;"></iframe>

In this example we go through the same 5 steps as in the staic example. First we locate the animation that we want. The hero animation is dynamic because it calculates the keyframes from the bounding boxes of the to and from elements (passed in by the configuration) right before it plays the animation.

The next step is to define our keyframes:

@keyframes hero {
  /* initial frame (dummy value will be changed) */
  0% {
    transform: translate(0px,0px) scale(0,0);
  },
	    
  /* end frame */
  100% {
    transform: none;
  }
}

this animation has a placeholder translate and scale that will be changed in the javascript. The next step is where we apply the animation to a css selector that will play the animation when applied to an element in the scope:

.heroing {
  /* name of predefined keyframe animation */
  animation-name: hero;
  animation-duration: .5s;
  /* animation reference frame is top left origin */
  transform-origin: 0 0;
}

The next step is tricky; this is where we have to create a playAnimation function that performs the bounding box calculations and sets the values in the style tag. Here is an example of the playAnimation function:

playAnimation() {
  ...
  this.generateAndSetKeyframes();
  ...
  this.to.classList.add('heroing');
}

The generateAndSetKeyframes function performs the same calculations in hero-animation.html, but it also sets the CSS keyframe:

__setKeyframeTransform(dL, dT, dW, dH) {
  // this.root.styleSheets does not work on Safari
  const sheets = this.root.querySelectorAll('style');

  if (!sheets) {
    return;
  }

  for (let i = 0; i < sheets.length; i++) {
    const sheet = sheets[i].sheet;

    for (let j = 0; j < sheet.cssRules.length; j++) {
      const rule = sheet.cssRules[j];

      if (rule instanceof CSSKeyframesRule && rule.name == 'hero') {
        rule.deleteRule('0%');
        rule.appendRule(`0% {transform: translate(${dL}px,${dT}px) scale(${dW},${dH});}`);
        return;
      }
    }
  }
}

Here, we find the style tag with the keyframe declaration, access the CSSStyleSheet interface, and edit the specific keyframe rule. This method is inefficient depending on the number of stylesheets and css rules; if you cannot easily find the style tag or rule (Polymer unfortunately removes the id of style tags), then CSS Keyframes are not for you.

The final step is to reset the animation with a listener on animationend which we do in the ready function:

this.root.addEventListener('animationend',
  _ => this.animating = false);

We now have a hero animation but with a gauche and inefficient keyframe update.

Web Animations API

Dynamic animations are a piece of 🍰 when it comes to the Web animations API because there is no hunting around for style rules, and the keyframe syntax gives great control over the animation.

Here is example code to a hero animation implemented with the Web Animations API.

<iframe src="https://nimble-stop.glitch.me/wa/hero.html" width="100%" height="300px" style="border:solid 1px black;"></iframe>

You will notice that the playAnimation function is significantly simpler than the css Keyframe animations:

playAnimation() {
  if (!this.animating) {
    // set property for data binding (observer will call
    // playAnimation again)
    this.animating = true;
    return;
  }

  this.to.style.visibility = 'visible';
  this.to.style.transformOrigin = '0 0';
  this.from.style.visibility = 'hidden';
  
  const keyframes = this.generateKeyframes();
  const animation = this.to.animate(keyframes, {
    duration: 500
  });
  
  animation.onfinish = _ => { this.animating = false };
}

The only "magic" that happens is the generateKeyframes function which only runs the logic from the config function.

CSS Transitions

Again, CSS Transitions are pretty simple when it comes to dynamic animations, and again most of the complexity comes when you are trying to have a step-by-step keyframe structure. Here is example code to a hero animation implemented with CSS Transitions.

<iframe src="https://nimble-stop.glitch.me/ttn/hero.html" width="100%" height="300px" style="border:solid 1px black;"></iframe>

The playAnimation function is a bit more-complex than the previous iterations:

playAnimation() {
  if (!this.animating) {
    // set property for data binding (observer will call
    // playAnimation again)
    this.animating = true;
    return;
  }
  
  const transformValue = this.generateTransform();
  // first "keyframe"
  this.to.style.transform = transformValue;

  this.to.style.visibility = 'visible';
  this.to.style.transformOrigin = '0 0';
  this.from.style.visibility = 'hidden';
  
  // "keyframe"-like state machine
  const transitionListener = _ => {
    switch (this.__animationStep) {
      // reset state to before animation (more keyframes can be added here)
      case 0:
        this.to.removeEventListener('transitionend', transitionListener);
        this.to.style.transition = null;
        this.to.style.transform = null;
        this.animating = false;
        this.__animationStep = 0;
        break;
    }
  }
  
  this.to.addEventListener('transitionend', transitionListener)
  
  // FF batches visibility and transform within a single requestAnimationFrame
  this.__afterNextRender(_ => {
    this.to.style.transition = 'transform .5s';
    // second "keyframe"
    this.to.style.transform = 'none';
  });
}

The generateTransform function runs the logic from the config function similar to the Web Animations API example, but then we set the initial state which is a transformed overlay. Then we make it visible, define our "keyframe" state machine (which in this case only resets the animation), listen for the transitions, then sets the transition and initiates the animation after the next render. We must wait for the next render because certain browsers batch layout changes like transitions and visibility, and elements that have 0 visibility cannot be animated.

What About Sharing Animations?

The Polymer team attempted to create a one-off solution that would animate anything for anyone in any condition, but it unfortunately became a gargantuan project that obfuscated the process rather than simplifying it. The web platform already has methods to animate anything for anyone in any condition; they're called CSS Keyframe Animations, CSS Transitions, and the Web Animations API.

Sharing animations can also be very useful but depends heavily on the layout of your project e.g. a project may choose to not use the Web Animations API, the project may use all static animations, the animations may need to trigger in a peculiar way, etc. Instead of locking you into the gargantuan technical debt that is neon-animation and locking you into the proprietary json configurations of these animations, we found that it makes more sense for each user to have a finer-grained control over their animations infrastructure.

Some general tips to sharing animations:

  • HTML Imports can package both CSS in style tags and JS in script tags, so it just makes sense to use them to package your animation keyframes / state machines.
  • CSS Keyframes can be applied like a shared style.
  • Web Animations API and CSS Transitions (and their keyframe-like state machines) can easily be packaged in a class or a mixin function.

Recommended Reading

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment