TL;DR
Utilisez des transformations de mise à l'échelle lorsque vous animez des extraits. Vous pouvez empêcher les enfants d'être étirés et déformés pendant l'animation en les redimensionnant.
Nous avons déjà publié des articles sur la création d'effets de parallaxe et de barres de défilement infini performants. Dans cet article, nous allons examiner ce qu'il faut faire pour obtenir des animations de clips performantes. Pour voir une démonstration, consultez le dépôt GitHub d'exemples d'éléments d'interface utilisateur.
Prenons l'exemple d'un menu déroulant:
Certaines options de création sont plus performantes que d'autres.
Mauvaise pratique: animer la largeur et la hauteur d'un élément de conteneur
Vous pouvez imaginer utiliser un peu de CSS pour animer la largeur et la hauteur de l'élément conteneur.
.menu {
overflow: hidden;
width: 350px;
height: 600px;
transition: width 600ms ease-out, height 600ms ease-out;
}
.menu--collapsed {
width: 200px;
height: 60px;
}
Le problème immédiat de cette approche est qu'elle nécessite d'animer width
et height
.
Ces propriétés nécessitent de calculer la mise en page et de peindre les résultats sur chaque frame de l'animation, ce qui peut être très coûteux et vous empêcher d'atteindre les 60 FPS. Si vous ne le saviez pas, consultez nos guides sur les performances de rendu pour en savoir plus sur le fonctionnement du processus de rendu.
Mauvaise pratique: utiliser les propriétés CSS "clip" ou "clip-path"
Au lieu d'animer width
et height
, vous pouvez utiliser la propriété clip
(désormais obsolète) pour animer l'effet d'expansion et de réduction. Si vous préférez, vous pouvez utiliser clip-path
à la place. Toutefois, l'utilisation de clip-path
est moins compatible que celle de clip
. Cependant, clip
est obsolète. Mais ne désespérez pas, ce n'est pas la solution que vous souhaitiez de toute façon.
.menu {
position: absolute;
clip: rect(0px 112px 175px 0px);
transition: clip 600ms ease-out;
}
.menu--collapsed {
clip: rect(0px 70px 34px 0px);
}
Bien que cette approche soit meilleure que l'animation des width
et height
de l'élément de menu, son inconvénient est qu'elle déclenche toujours la peinture. De plus, la propriété clip
, si vous choisissez cette méthode, nécessite que l'élément sur lequel elle opère soit positionné de manière absolue ou fixe, ce qui peut nécessiter un peu de travail supplémentaire.
Bon: échelles d'animation
Comme cet effet implique que quelque chose devient de plus en plus grand, vous pouvez utiliser une transformation à l'échelle. C'est une excellente nouvelle, car la modification des transformations ne nécessite pas de mise en page ni de peinture, et le navigateur peut la transmettre au GPU, ce qui signifie que l'effet est accéléré et qu'il est beaucoup plus susceptible d'atteindre 60 FPS.
L'inconvénient de cette approche, comme la plupart des aspects liés aux performances de rendu, est qu'elle nécessite un peu de configuration. Mais le jeu en vaut la chandelle !
Étape 1: Calculer les états de début et de fin
Avec une approche qui utilise des animations de mise à l'échelle, la première étape consiste à lire les éléments qui indiquent la taille que le menu doit avoir à la fois lorsqu'il est réduit et lorsqu'il est développé. Dans certains cas, il se peut que vous ne puissiez pas obtenir ces deux informations en une seule fois et que vous deviez, par exemple, basculer certaines classes pour pouvoir lire les différents états du composant.
Toutefois, soyez prudent si vous devez procéder ainsi: getBoundingClientRect()
(ou offsetWidth
et offsetHeight
) oblige le navigateur à exécuter les styles et les passes de mise en page si les styles ont changé depuis leur dernière exécution.
function calculateCollapsedScale () {
// The menu title can act as the marker for the collapsed state.
const collapsed = menuTitle.getBoundingClientRect();
// Whereas the menu as a whole (title plus items) can act as
// a proxy for the expanded state.
const expanded = menu.getBoundingClientRect();
return {
x: collapsed.width / expanded.width,
y: collapsed.height / expanded.height
};
}
Dans le cas d'un menu, nous pouvons raisonnablement supposer qu'il commence à l'échelle naturelle (1, 1). Cette échelle naturelle représente son état développé. Vous devrez donc animer à partir d'une version réduite (calculée ci-dessus) jusqu'à cette échelle naturelle.
Une question se pose. Cela devrait également redimensionner le contenu du menu, n'est-ce pas ? Eh bien, comme vous pouvez le voir ci-dessous, oui.
Que pouvez-vous faire ? Vous pouvez appliquer une contre-transformation au contenu. Par exemple, si le conteneur est réduit à un cinquième de sa taille normale, vous pouvez augmenter son contenu de cinq fois pour éviter qu'il ne soit écrasé. Deux points sont à noter:
La contre-transformation est également une opération d'échelle. C'est bien, car il peut également être accéléré, tout comme l'animation du conteneur. Vous devrez peut-être vous assurer que les éléments animés obtiennent leur propre couche de composition (ce qui permet au GPU de vous aider). Pour ce faire, vous pouvez ajouter
will-change: transform
à l'élément ou, si vous devez prendre en charge les anciens navigateurs,backface-visiblity: hidden
.La contre-transformation doit être calculée par frame. À ce stade, les choses peuvent devenir un peu plus compliquées. En effet, en supposant que l'animation est en CSS et qu'elle utilise une fonction de lissage de vitesse, ce dernier doit être contraint lors de l'animation de la contre-transformation. Toutefois, le calcul de la courbe inverse pour, disons,
cubic-bezier(0, 0, 0.3, 1)
n'est pas si évident.
Il peut donc être tentant d'envisager d'animer l'effet à l'aide de JavaScript. Après tout, vous pouvez ensuite utiliser une équation d'atténuation pour calculer les valeurs d'échelle et de contre-échelle par frame. L'inconvénient de toute animation basée sur JavaScript est ce qui se passe lorsque le thread principal (sur lequel votre code JavaScript s'exécute) est occupé par une autre tâche. La réponse courte est que votre animation peut bégayer ou s'arrêter complètement, ce qui n'est pas idéal pour l'expérience utilisateur.
Étape 2 : Créer des animations CSS en temps réel
La solution, qui peut sembler étrange au premier abord, consiste à créer dynamiquement une animation avec des clés d'animation et à l'injecter dans la page pour qu'elle soit utilisée par le menu. (Un grand merci à l'ingénieur Chrome Robert Flack pour nous avoir signalé ce problème.) L'avantage principal est qu'une animation avec des clés-images qui transforme les transformations peut être exécutée sur le compositeur, ce qui signifie qu'elle n'est pas affectée par les tâches du thread principal.
Pour créer l'animation de la clé-image, nous passons de 0 à 100 et calculons les valeurs d'échelle requises pour l'élément et son contenu. Ceux-ci peuvent ensuite être réduits à une chaîne, qui peut être injectée dans la page en tant qu'élément de style. L'injection des styles entraîne une étape de calcul des styles sur la page, ce qui représente une tâche supplémentaire que le navigateur doit effectuer, mais il ne le fera qu'une seule fois au démarrage du composant.
function createKeyframeAnimation () {
// Figure out the size of the element when collapsed.
let {x, y} = calculateCollapsedScale();
let animation = '';
let inverseAnimation = '';
for (let step = 0; step <= 100; step++) {
// Remap the step value to an eased one.
let easedStep = ease(step / 100);
// Calculate the scale of the element.
const xScale = x + (1 - x) * easedStep;
const yScale = y + (1 - y) * easedStep;
animation += `${step}% {
transform: scale(${xScale}, ${yScale});
}`;
// And now the inverse for the contents.
const invXScale = 1 / xScale;
const invYScale = 1 / yScale;
inverseAnimation += `${step}% {
transform: scale(${invXScale}, ${invYScale});
}`;
}
return `
@keyframes menuAnimation {
${animation}
}
@keyframes menuContentsAnimation {
${inverseAnimation}
}`;
}
Les plus curieux se demanderont peut-être pourquoi la fonction ease()
est utilisée dans la boucle for. Vous pouvez utiliser quelque chose comme ceci pour mapper les valeurs de 0 à 1 sur un équivalent atténué.
function ease (v, pow=4) {
return 1 - Math.pow(1 - v, pow);
}
Vous pouvez également utiliser la recherche Google pour visualiser ce que cela donne. Pratique ! Si vous avez besoin d'autres équations d'atténuation, consultez Tween.js de Soledad Penadés, qui en contient un grand nombre.
Étape 3 : Activez les animations CSS
Comme ces animations sont créées et intégrées à la page en JavaScript, la dernière étape consiste à activer/désactiver les classes pour activer les animations.
.menu--expanded {
animation-name: menuAnimation;
animation-duration: 0.2s;
animation-timing-function: linear;
}
.menu__contents--expanded {
animation-name: menuContentsAnimation;
animation-duration: 0.2s;
animation-timing-function: linear;
}
Les animations créées à l'étape précédente s'exécutent alors. Étant donné que les animations cuites sont déjà lissées, la fonction de temporisation doit être définie sur linear
, sinon vous lisserez entre chaque image clé, ce qui sera très étrange.
Pour réduire à nouveau l'élément, vous avez deux options : mettre à jour l'animation CSS pour qu'elle s'exécute à l'envers plutôt qu'en avant. Cela fonctionne très bien, mais l'impression de l'animation sera inversée. Par conséquent, si vous avez utilisé une courbe de décélération, l'inversion sera lente, ce qui la rendra lente. Une solution plus appropriée consiste à créer une deuxième paire d'animations pour réduire l'élément. Vous pouvez les créer exactement de la même manière que les animations d'image clé de développement, mais avec des valeurs de début et de fin interverties.
const xScale = 1 + (x - 1) * easedStep;
const yScale = 1 + (y - 1) * easedStep;
Version plus avancée : révélations circulaires
Il est également possible d'utiliser cette technique pour créer des animations d'expansion et de repli circulaires.
Les principes sont en grande partie les mêmes que dans la version précédente, où vous mettez à l'échelle un élément et contre-échellez ses enfants immédiats. Dans ce cas, l'élément qui est mis à l'échelle a une valeur border-radius
de 50 %, ce qui le rend circulaire et est enveloppé par un autre élément qui a overflow: hidden
, ce qui signifie que vous ne voyez pas le cercle s'étendre en dehors des limites de l'élément.
Avertissement concernant cette variante particulière : le texte de Chrome est flou sur les écrans à faible résolution au cours de l'animation en raison d'erreurs d'arrondi dues à la mise à l'échelle et à la contre-mise à l'échelle du texte. Si vous souhaitez en savoir plus, un bug a été signalé et vous pouvez l'ajouter à vos favoris et le suivre.
Le code de l'effet d'expansion circulaire est disponible dans le dépôt GitHub.
Conclusions
Vous avez donc un moyen de créer des animations de clip vidéo performantes à l'aide de transformations d'échelle. Dans un monde parfait, il serait préférable que les animations d'extrait soient accélérées (il existe un bug Chromium pour cela créé par Jake Archibald). En attendant, soyez prudent lorsque vous animez clip
ou clip-path
, et évitez d'animer width
ou height
.
Il est également utile d'utiliser des animations Web pour des effets comme celui-ci, car elles disposent d'une API JavaScript, mais peuvent s'exécuter sur le thread du moteur de rendu si vous n'animez que transform
et opacity
.
Malheureusement, l'intégration des animations Web n'est pas optimale, mais vous pouvez utiliser l'amélioration progressive pour les utiliser si elles sont disponibles.
if ('animate' in HTMLElement.prototype) {
// Animate with Web Animations.
} else {
// Fall back to generated CSS Animations or JS.
}
En attendant, même si vous pouvez utiliser des bibliothèques basées sur JavaScript pour créer une animation, vous constaterez peut-être que vous obtenez des performances plus fiables en créant une animation CSS et en l'utilisant à la place. De même, si votre application repose déjà sur JavaScript pour ses animations, il peut être préférable d'être au moins cohérent avec votre codebase existant.
Si vous souhaitez examiner le code de cet effet, consultez le dépôt GitHub des exemples d'éléments d'interface utilisateur. Comme toujours, n'hésitez pas à nous faire part de vos commentaires ci-dessous.