10 things I hate about animation
(and how to fix them)

Mozilla Roadshow 2019
Brian Birtles, Birchill, Inc.

Self-introduction

  • 2004~2019
  • SVG WG 2011~
    CSS WG 2015~
  • 2019~

10 reasons why animations are…

  • Hard
  • Buggy
  • Sluggish
  • Frustrating

#1: It's hard to do frame-based animation


1
2
3
4


5
6
7


.filmstrip {
  animation: slide 1s infinite steps(7);
}
@keyframes slide {
  to { transform: translate(-700px); }
}

.filmstrip {
  /* steps(7) = 7 "changes" including the last one */
  animation: slide 1s infinite steps(7);
}
@keyframes slide {
  to { transform: translate(-700px); }
}

.filmstrip {
  animation: slide 1s infinite steps(6);
}
@keyframes slide {
  to { transform: translate(-700px); }
}

.filmstrip {
  /* steps(6) = steps(6, end) */
  animation: slide 1s infinite steps(6);
}
@keyframes slide {
  to { transform: translate(-700px); }
}

.filmstrip {
  animation: slide 1s infinite steps(6, start);
}
@keyframes slide {
  to { transform: translate(-700px); }
}

.filmstrip {
  /* New! jump-none! */
  animation: slide 1s infinite steps(7, jump-none);
}
@keyframes slide {
  to { transform: translate(-700px); }
}

CSS Easing Functions

jump-none, jump-both

  • 65+
  • 77+
  • x
  • x

#2: Transitions are buggy

1. Sometimes transitions don't run 2. Sometimes transitions don't finish

New events!

  • transitionrun

    → a transition was created 🆕

    → ok to wait for transitionend 🙆‍♂️

  • transitioncancel

    → element vanished 🗑️ (dropped, re-“rendered”, display:none etc.)

    → don't wait for transitionend

  • transitionstart (to match animationstart)
  • animationcancel (as with transitioncancel)

transitioncancel and friends

  • 53+
  • 74+ No animationcancel
  • Tech Preview
  • x Only transitionstart
  • No animationcancel

#3: I ❤️ JS

This slide requires support for Element.animate.

CSS animations from JS


const uniqueKeyframesName = generateUuid();
document.styleSheets[0].insertRule(
  `@keyframes ${uniqueKeyframesName} {
    from { transform: scale(0) }
    95% { transform: scale(${fullSize}) }
    to { transform: scale(0) }
  }`
);
star.style.animation =
  `${uniqueKeyframesName} ${duration}ms ` +
  `${delay}ms infinite`;

// Don't forget to clean up!

CSS transitions from JS

  • transitionrun, transitioncancel etc.
  • Triggering transitions from JS is hard
https://birtles.github.io/cssconf2019/

…using Element.animate()


star.animate(
  [
    { transform: 'scale(0)' },
    { transform: `scale(${fullSize})`, offset: 0.95 },
    { transform: 'scale(0)' },
  ],
  { easing, duration, delay, iterations: Infinity }
);

…using Element.animate()


const starAnimation = star.animate(
  [
    { transform: 'scale(0)' },
    { transform: `scale(${fullSize})`, offset: 0.95 },
    { transform: 'scale(0)' },
  ],
  { easing, duration, delay, iterations: Infinity }
);

// starAnimation.reverse();
// starAnimation.cancel();
// starAnimation.updatePlaybackRate(0.5);

starAnimation.finished.then(() => { ... });

Element.animate()

  • 48+
  • 36+
  • Tech Preview
  • x

#4: Easing the whole animation is hard


.body, .head {
  animation-timing-function: steps(4);
}

.head {
  animation-timing-function: steps(4);
}
@keyframes drop-head {
  60% { transform: rotate(45deg) }
}
  

@keyframes drop-head {
  0% {
    transform: none;
    animation-timing-function: steps(4);
  }
  50% {
    transform: rotate(45deg);
    animation-timing-function: steps(4);
  }
  100% { transform: none; }
}
  

head.animate(
  {
    // Implicit 0%/100% keyframes are not yet
    // shipping in Chrome/Firefox
    transform: ['none', 'rotate(60deg)', 'none'],
    offset: [0, 0.6, 1],
  },
  {
    duration: 2000,
    iterations: Infinite,
    easing: 'steps(4)'
  }
);
This slide requires support for Element.animate.

head.animate(
  {
    transform: ['none', 'rotate(60deg)', 'none'],
    offset: [0, 0.6, 1],
    easing: 'ease-in',
  },
  {
    duration: 2000,
    iterations: Infinite,
    easing: 'steps(4)'
  }
);
This slide requires support for Element.animate.

#5: You can't define animations by speed instead of time

CSS “Working” Group

Hurry up, add speed-based animations!

Element.getAnimations()


document.addEventListener('transitionrun', evt => {
  if (evt.propertyName !== 'transform') {
    return;
  }

  const transition = evt.target
    .getAnimations()
    .find(animation => animation.transitionProperty === 'transform');

  const keyframes = transition.effect.getKeyframes();
  const distance = calculateDistance(
    keyframes[0].transform,
    keyframes[1].transform
  );

  // Move at 700px per second
  const duration = (distance / 700) * 1000;
  transition.effect.updateTiming({ duration });
  // NOTE: `transition.updatePlaybackRate()` would be even better
});
This slide requires support for Element.getAnimations.

Element.getAnimations()

  • Nightly
  • Canary
  • Tech Preview
  • x
  • Canary

#6: You can't define transitions with midpoints

rgb(255, 0, 0)
rgb(?, ?, ?) rgb(128, 64, 0)
rgb(0, 128, 0)
😧
Color #1:
Color #2:
rgb(255, 0, 0)
rgb(255, 255, 0)
rgb(0, 128, 0)
🤔
hsl(0, 100%, 50%)
hsl(60, 100%, 37.5%)
hsl(120, 100%, 25%)

CSS “Working” Group

Hurry up, add more interpolation modes!

CSSTransition.setKeyframes()


document.addEventListener('transitionrun', evt => {
  if (evt.propertyName !== 'fill') {
    return;
  }

  const transition = evt.target
    .getAnimations()
    .find(animation => animation.transitionProperty === 'fill');

  const keyframes = transition.effect.getKeyframes();
  const hslKeyframes = generateHslKeyframes(
    keyframes[0].fill,
    keyframes[1].fill
  );

  transition.effect.setKeyframes(hslKeyframes);
});
Color #1:
Color #2:
This slide requires support for Element.getAnimations.

CSSTransition.setKeyframes() (Element.getAnimations)

  • Nightly
  • Canary
  • Tech Preview
  • x
  • Canary

#7: I want to fly

This slide requires support for offset-path.

offset-path


.goose {
  offset-path: path('M100 100...');
  animation: fly 10s linear infinite;
}
@keyframes fly {
  to { offset-distance: 100%; }
}

offset-path

  • 72+
  • 55+ (⚠️ offset-anchor preffed off)
  • x
  • x
  • (⚠️ offset-anchor preffed off)

#8: Performance is painful

transform and opacity

move
grow
spin
3D
fade
grow + spin + fade
color change
shadow effect
fly
This slide requires support for offset-path.

Firefox Animation DevTools

#9: Animations give me a headache

Sick of Animation

Headaches, dizziness, migraines, epilepsy…
Windows
Mac
prefers-reduced-motion: reduce | no-preference

div {
  animation: ...
  transition: ...
}

@media (prefers-reduced-motion: reduce) {
  * {
    animation-name: none !important;
    transition-property: none !important;

    /* Or to ensure events still fire, e.g.
     *  transition-duration: 0.01s !important;
     *  transition-delay: 0s !important;
     *  ...
     */
  }
}

From JS…


// Check for browser support
if (!('animate' in elem)) {
  return;
}

// Check for user support
if (matchMedia('(prefers-reduced-motion)').matches) {
  return;
}

// Animate away...

prefers-reduced-motion

  • 64+
  • 74+
  • 10.1+
  • x

10 things I hate about animations…

  1. Frame-based animations… jump-none
  2. Transition bugs… transitioncancel
  3. Creating animations from JS… Element.animate
  4. Easing the whole animation… Element.animate
  5. Defining animation speedElement.getAnimations
  6. Transition midpointsElement.getAnimations
  7. Flying… offset-path
  8. Performance… Firefox Animation DevTools
  9. Headaches… prefers-reduced-motion
  1. …they are too much fun!

Thank you!