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
Les animations d'aujourd'hui avec WAAPI - Paris Web 2020
By Louis Hoebregts
Les animations d'aujourd'hui avec WAAPI - Paris Web 2020
Slides from my talk at Paris Web 2020 about the Web Animations API
- 1,522