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'implique la création d'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 de 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
. Toutefois, 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
Étant donné que cet effet implique un agrandissement et un rétrécissement, vous pouvez utiliser une transformation de mise à 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 pour la plupart des éléments des performances de rendu, est qu'elle nécessite un peu de configuration. Mais ça vaut vraiment le coup !
É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é. Il est possible que, dans certaines situations, vous ne puissiez pas obtenir ces deux éléments d'informations en une seule fois et que vous deviez, par exemple, activer/désactiver certaines classes pour pouvoir lire les différents états du composant.
Toutefois, si vous devez le faire, soyez prudent: getBoundingClientRect()
(ou offsetWidth
et offsetHeight
) force le navigateur à exécuter des passes de style et 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é, ce qui signifie que vous devrez 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 agrandir le 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 image. C'est là que les choses peuvent devenir un peu plus délicates, car en supposant que l'animation soit en CSS et utilise une fonction d'atténuation, l'atténuation elle-même doit être contrecarrée 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 de mise à l'échelle et de contre-mise à l'é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. Ils 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 obtenir un aperçu. 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
Une fois ces animations créées et intégrées à la page en JavaScript, l'étape finale consiste à activer les animations en activant les classes.
.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'images clés d'expansion, mais en échangeant les valeurs de début et de fin.
const xScale = 1 + (x - 1) * easedStep;
const yScale = 1 + (y - 1) * easedStep;
Version plus avancée: révélations circulaires
Vous pouvez également utiliser cette technique pour créer des animations circulaires d'expansion et de réduction.
Les principes sont en grande partie les mêmes que dans la version précédente, où vous redimensionnez un élément et que vous redimensionnez 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 savez maintenant comment créer des animations de clips performantes à l'aide de transformations de mise à l'échelle. Dans un monde idéal, il serait idéal d'accélérer les animations de clips (un bug Chromium à cet effet a été créé par Jake Archibald), mais en attendant, vous devez être prudent lorsque vous animez clip
ou clip-path
, et éviter 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 de rester 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.