Les animations

d'aujourd'hui

avec WAAPI

Louis Hoebregts

 

1996

Adobe Flash Player

<svg>
  <filter>
    <feTurbulence baseFrequency="0.02" numOctaves="3" result="n" seed="0">
      <animate attributeName="seed" from="1" to="9" dur="1s" repeatCount="indefinite" />
    </feTurbulence>
    <feDisplacementMap in="SourceGraphic" in2="n" scale="6" />
  </filter>
</svg>

1998

SVG SMIL

2006

jQuery

  • $(el).fadeIn()
  • $(el).fadeOut()
  • $(el).animate()
  • $(el).slideDown()
  • $(el).slideUp()
@keyframes spin {
  to {
    transform: rotate(1turn);
  }
}
.eye {
  transform-box: fill-box;
  transform-origin: center;
  animation: 1s spin linear infinite;
}
.eye.left {
  animation-direction: reverse;
}

2009

CSS Animations

function render () {
  requestAnimationFrame(render);
  ctx.clearRect(0, 0, width, height);
  spines.forEach(spine => {
    const spine = spines[i];
    spine.update();
    spine.draw();
  });
}
requestAnimationFrame(render);

2011

RequestAnimationFrame

2012

GreenSock

WAAPI

Web Animations Application Programming Interface

Web Animations API

2013

@keyframes breathe {
  0% {
    transform: scale(1);
    background: hotpink;
  }
  50% {
    background: darkorange;
  }
  100% {
    transform: scale(1.5);
    background: greenyellow;
  }
}

div {
  animation: breathe 3s infinite alternate ease-in-out;
}
element.animate(
  keyframes,
  options
);

Animate

Keyframes

const keyframes = [
  {
    transform: 'scale(1)',
    background: 'hotpink'
  },
  {
    background: 'darkorange'
  },
  {
    transform: 'scale(1.5)',
    background: 'greenyellow'
  }
];

Keyframes

const keyframes = [
  {
    background: 'hotpink'
  },
  {
    background: 'darkorange'
  },
  {
    transform: 'scale(1.5)',
    background: 'greenyellow'
  }
];

Keyframes

const keyframes = {
  transform: ['scale(1)', 'scale(1.5)'],
  background: ['hotpink', 'darkorange', 'greenyellow']
};
const keyframes = {
  transform: ['scale(1.5)'],
  background: ['hotpink', 'darkorange', 'greenyellow']
};

Keyframes Offset

[{
  background: 'orchid',
  // offset: 0
}, {
  background: 'darkorange',
  offset: 0.8
}, {
  background: 'greenyellow',
  // offset: 1
}]
{
  background: ['orchid', 'darkorange', 'greenyellow'],
  offset: [0, 0.8]
}
@keyframes shades {
  0% {
    background: orchid;
  }
  80% {
    background: darkorange;
  }
  100% {
    background: greenyellow;
  }
}

WAAPI

CSS

WAAPI

Options

const options = {
  duration: 3000,
  iterations: Infinity,
  direction: 'alternate',
  easing: 'ease-in-out'
};
  • delay
  • direction
  • duration
  • easing
  • endDelay
  • fill
  • iterationStart
  • iterations

CSS

WAAPI

animation-duration: 2s, 2000ms;
duration: 3000
animation-fill-mode: forwards;
fill: 'forwards'
animation-timing-function: ease-out;
easing: 'ease-out'
animation-iteration-count: infinite;
iterations: Infinity
¯\_(ツ)_/¯
iterationStart: 1
const keyframes = {
  transform: ['scale(1.5)'],
  background: ['hotpink', 'darkorange', 'greenyellow']
};

const options = {
  duration: 3000,
  iterations: Infinity,
  direction: 'alternate',
  easing: 'ease-in-out'
};

document.querySelector('.box').animate(keyframes, options);
const element = document.querySelector('.box');

const keyframes = {
  transform: ['scale(1.5)'],
  background: ['hotpink', 'darkorange', 'greenyellow']
};

const options = {
  duration: 3000,
  iterations: Infinity,
  direction: 'alternate',
  easing: 'ease-in-out'
};


const effect = new KeyframeEffect(element, keyframes, options);
const animation = new Animation(effect);
animation.play();

KeyframeEffect & Animation

Controls

 

Controls

// Solution 1
const animation = document.querySelector('.box').animate({
  transform: ['scale(1.5)'],
  background: ['hotpink', 'darkorange', 'greenyellow']
}, {
  duration: 2000,
  fill: 'forwards'
});

// Solution 2
const animation = new Animation(keyframeEffect);

document.querySelector('.pause').addEventListener('click', () => {
  animation.pause();
});

document.querySelector('.play').addEventListener('click', () => {
  animation.play();
});
animation.oncancel = () => console.log('Cancelled');
animation.onfinish = () => console.log('Finished');

Events & Properties

animation.currentTime;
animation.playbackRate;
animation.playState; // Read only

Expand - Collapse

Situation #1

Expand - Collapse

 

Details - Summary

<details>
  <summary>Click to expand this details</summary>
  <div class="content">
    <p>
      Lorem, ipsum dolor sit amet...
    </p>
    <img src="bear.jpg" alt="A brown bear in a river">
    <p>
      Facilis ducimus iure officia quos possimus...
    </p>
  </div>
</details>

Dropdown - CSS Fade-in

details[open] .content {
  animation: fade-in 0.6s ease-in-out;
}

@keyframes fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
} 

Dropdown - WAAPI

class Dropdown {
  constructor(el) {}
  onClick() {
    this.el.style.overflow = 'hidden';
    this.el.open ? this.shrink() : this.open();
  }
  open() {
    this.el.style.height = `${this.el.offsetHeight}px`;
    requestAnimationFrame(() => this.expand());
  }
  expand() {
    const startHeight = `${this.el.offsetHeight}px`;
    const endHeight = `${this.summary.offsetHeight + this.content.offsetHeight}px`;
    this.el.animate({
      height: [startHeight, endHeight]
    }, { duration: 300 })
    .onfinish = () => this.onAnimationFinish();
  }
  onAnimationFinish() {
    this.el.style.height = this.el.style.overflow = '';
  }
}

Particules

Situation #2

Particles

 

Particles - WAAPI

const particle = document.createElement('span');
particle.classList.add('particle');
document.body.appendChild(particle);

const destinationX = e.clientX + (Math.random() - 0.5) * innerWidth;
const destinationY = e.clientY + (Math.random() - 0.8) * innerHeight;

particle.animate({
  transform: [
    `translate(${e.clientX}px, ${e.clientY}px)`,
    `translate(${destinationX}px, ${destinationY}px)`
  ],
  opacity: [1, 1, 0],
  offset: [0, 0.7]
}, {
  duration: 1000 + Math.random() * 1000,
  easing: 'cubic-bezier(0, .9, .57, 1)',
  delay: Math.random() * 200
})
.onfinish = () => particle.remove();

Vue.js

Situation #3

Vue.js

 

Vue.js - WAAPI

Vue.component("list-item", {
  props: ["index", "text"],
  template: `<li>
      <span>{{ text }}</span>
      <button @click='onDeleteClick($event, index)'>Remove</button>
    </li>`,
  methods: {
    onDeleteClick: function (event, index) {
      this.$el.animate({
        height: [`${this.$el.offsetHeight}px`, "0px"]
      }, {
        duration: 400,
        easing: "ease-out"
      }).onfinish = () => app.list.splice(index, 1);
    }
  }
});

Browser support

Browser support

08.08.2020
 

Browser support detection

if (document.body.animate) {
  // WAAPI is supported by the browser
}
// WAAPI is not supported by the browser
if (!document.body.animate) return;

Prefers reduced motion

Prefers reduced motion

iOs

Windows

Prefers reduced motion

08.08.2020
 

Prefers reduced motion - WAAPI

 

Prefers reduced motion - WAAPI

const fadein = new KeyframeEffect(box,
  {
    opacity: [0, 1]
  }, {
    duration: 500,
    easing: 'ease-out'
  }
);

const slidein = new KeyframeEffect(box,
  {
    transform: ['translateY(-100vh)', 'translateY(0vh)'],
    opacity: [0, 1]
  }, {
    duration: 500,
    easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)'
  }
);

if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
  new Animation(fadein, document.timeline).play();
} else {
  new Animation(slidein, document.timeline).play();
}

+

̶ 

  • Intégré dans le navigateur
  • Plus de contrôle que CSS
  • Animations plus complexes
  • Support navigateurs récents
  • Support irrégulier
  • Limitations CSS
  • Work in progress

Avantages & Inconvénients

Et dans le futur?

ScrollTimeline

 

ScrollTimeline - WAAPI

const scrolltimeline = new ScrollTimeline({
  source: document.documentElement,
  timeRange: 2000,
  startScrollOffset: '50vh',
  endScrollOffset: '150vh'
});
new Animation(keyframeEffect, scrolltimeline).play();
Chrome & Edge behind flag - 08.08.2020

Custom easings

 

Bounce, Elastic,...

Grouped animations

 

Thank you

© Cacti illustrations from Freepik