Créer des animations d'expansion et de réduction performantes

Paul Lewis
Stephen McGruer
Stephen McGruer

Résumé

Utilisez des transformations d'échelle lorsque vous animez des extraits. Vous pouvez empêcher les enfants d'être étirés et déformés pendant l'animation en effectuant un contre-scaling.

Nous avons précédemment publié des informations sur la création d'effets parallaxes et de défileurs infinis performants. Dans cet article, nous allons passer en revue ce que vous devez faire si vous souhaitez des animations d'extraits vidéo performantes. Si vous souhaitez 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 permettant de les créer sont plus performantes que d'autres.

Non: la largeur et la hauteur d'un élément conteneur sont animées.

Vous pouvez imaginer l'utilisation d'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 d'afficher les résultats sur chaque image de l'animation, ce qui peut être très coûteux et vous fait généralement passer à côté de 60 FPS. Si c'est votre cas, consultez nos guides sur les performances d'affichage, qui fournissent plus d'informations sur le fonctionnement du processus d'affichage.

Mauvaise installation: utilisez les propriétés CSS "clip-path" ou "clip-path"

Plutôt que d'animer width et height, vous pouvez utiliser la propriété clip (désormais obsolète) pour animer l'effet d'expansion et de repli. Vous pouvez également utiliser clip-path à la place. L'utilisation de clip-path, en revanche, est moins bien prise en charge que clip. Cependant, clip est obsolète. Bien. Mais ne désespérez pas, ce n'est pas la solution que vous attendiez 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 cela soit préférable à l'animation des éléments width et height de l'élément de menu, l'inconvénient de cette approche est qu'elle déclenche toujours l'affichage. De plus, si vous suivez cette route, la propriété clip nécessite que l'élément sur lequel il opère soit positionné de manière absolue ou fixe, ce qui peut nécessiter une préparation supplémentaire.

Bon: animer les échelles

Étant donné que cet effet implique de plus en plus petits, vous pouvez utiliser une transformation d'échelle. C'est une excellente nouvelle, car la modification des transformations ne nécessite pas de mise en page ni de peinture et que le navigateur peut 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 aspects des performances d'affichage, est qu'elle nécessite un peu de configuration. Cela en vaut vraiment la peine !

Étape 1: Calculez 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 vous indiquent la taille du menu à la fois 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, activer/désactiver certaines classes pour pouvoir lire les différents états du composant. Si vous devez procéder ainsi, soyez prudent: getBoundingClientRect() (ou offsetWidth et offsetHeight) oblige le navigateur à exécuter des styles et des passes de mise en page si les styles ont été modifiés 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, par exemple, nous pouvons partir du principe qu'il commencera à être dans son échelle naturelle (1, 1). Cette échelle naturelle représente son état développé, ce qui signifie que vous devez relancer l'animation à partir d'une version réduite (calculée ci-dessus) jusqu'à cette échelle naturelle.

Une question se pose. Bien sûr, cela augmenterait également le contenu du menu, n’est-ce pas ? Eh bien, comme vous pouvez le voir ci-dessous, oui.

Que pouvez-vous faire à ce sujet ? Vous pouvez appliquer une transformation counter-au contenu. Par exemple, si le conteneur est réduit à un quart de sa taille normale, vous pouvez augmenter par cinq le contenu pour éviter que le contenu ne soit écrasé. Il y a deux choses à noter à ce sujet:

  1. La contre-transformation est également une opération de mise à l'échelle. C'est une bonne, car elle peut également être accélérée, tout comme l'animation du conteneur. Vous devrez peut-être vous assurer que les éléments animés disposent de leur propre couche de compositeur (ce qui permet au GPU d'aider). Pour cela, vous pouvez ajouter will-change: transform à l'élément ou, si vous devez prendre en charge des navigateurs plus anciens, backface-visiblity: hidden.

  2. La contre-transformation doit être calculée par image. C'est là que les choses peuvent devenir un peu plus délicates. En effet, en supposant que l'animation est en CSS et qu'elle utilise une fonction de lissage de vitesse, le lissage de vitesse doit être contrôlé lors de l'animation de la contre-transformation. Toutefois, le calcul de la courbe inverse pour cubic-bezier(0, 0, 0.3, 1) n'est pas tout à fait é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 de lissage de vitesse pour calculer les valeurs d'échelle et d'échelle de compteur par image. L'inconvénient de toute animation basée sur JavaScript est ce qui se produit lorsque le thread principal (où s'exécute JavaScript) est occupé par une autre tâche. La réponse courte est que votre animation peut saccader ou s'arrêter complètement, ce qui n'est pas idéal pour l'UX.

Étape 2: Créez des animations CSS à la volée

La solution, qui peut sembler étrange au premier abord, consiste à créer une animation basée sur des images clés avec notre propre fonction de lissage de vitesse de manière dynamique, puis à l'injecter dans la page pour l'utiliser par le menu. (Un grand merci à l'ingénieur Chrome Robert Flack pour avoir signalé ceci !) Le principal avantage est qu'une animation basée sur des images clés qui modifie 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 d'image clé, nous passons de 0 à 100, et calculons les valeurs d'échelle nécessaires pour l'élément et son contenu. Celles-ci peuvent ensuite être réduites en une chaîne, qui peut être injectée dans la page en tant qu'élément de style. L'injection des styles entraîne le transfert de la fonction "Recalculer les styles" sur la page, ce qui représente un travail supplémentaire que le navigateur doit effectuer, mais qui ne l'effectue 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 curieux s'interrogent peut-être sur la fonction ease() dans la boucle For. Vous pouvez utiliser une solution de ce type pour mapper les valeurs de 0 à 1 avec un équivalent simplifié.

function ease (v, pow=4) {
  return 1 - Math.pow(1 - v, pow);
}

Vous pouvez également utiliser la recherche Google pour voir à quoi cela ressemble. Pratique ! Si vous avez besoin d'autres équations de lissage de vitesse, consultez Tween.js de Soledad Penadés, qui en contient un tas.

Étape 3: Activez les animations CSS

Maintenant que ces animations sont créées et intégrées à la page en JavaScript, la dernière étape consiste à activer/désactiver les classes qui les activent.

.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 intégrées ont déjà été lissées, la fonction de minutage doit être définie sur linear. Sinon, vous risquez de passer plus facilement entre chaque image clé, ce qui sera très étrange.

Pour réduire l'élément vers le bas, deux options s'offrent à vous: mettre à jour l'animation CSS pour qu'elle s'exécute dans l'ordre inverse plutôt que vers l'avant. Cela fonctionnera très bien, mais l'effet inverse de l'animation sera inversé. Si vous avez utilisé une courbe de lissage à l'éloignement, l'inverse sera atténué à l'intérieur, ce qui donnera l'impression que l'animation est 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 inversées.

const xScale = 1 + (x - 1) * easedStep;
const yScale = 1 + (y - 1) * easedStep;

Une version plus avancée: l'affichage circulaire

Vous pouvez également utiliser cette technique pour créer des animations circulaires de développement et de réduction.

Les principes sont en grande partie les mêmes que dans la version précédente, dans laquelle vous effectuez la mise à l'échelle d'un élément et celle de ses enfants immédiats. Dans ce cas, l'élément qui est mis à l'échelle a une border-radius de 50%, ce qui le rend circulaire, et est encapsulé par un autre élément avec overflow: hidden, ce qui signifie que vous ne voyez pas le cercle se développer en dehors des limites de l'élément.

Avertissement concernant cette variante particulière: Chrome présente du texte flou sur les écrans à faible PPP pendant l'animation en raison d'erreurs d'arrondi dues à l'échelle et à l'échelle du compteur de texte. Si les détails vous intéressent, un bug a été signalé. Vous pouvez le suivre et le suivre.

Le code de l'effet d'expansion circulaire est disponible dans le dépôt GitHub.

Conclusions

Vous disposez désormais d'un moyen de créer des animations d'extraits à l'aide de transformations de scaling. Dans un monde parfait, il serait intéressant que les animations d'extraits soient accélérées (il existe un bug dans Chromium créé par Jake Archibald). Mais avant d'y arriver, vous devez être prudent lorsque vous animez clip ou clip-path, et évitez vivement d'animer width ou height.

Il serait également utile d'utiliser des animations Web pour ce type d'effets, car ils disposent d'une API JavaScript, mais peuvent s'exécuter sur le thread du compositeur si vous n'animez que transform et opacity. Malheureusement, la compatibilité avec les animations Web n'est pas optimale, mais vous pourriez utiliser une 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, bien que vous puissiez utiliser des bibliothèques JavaScript pour effectuer l'animation, vous obtiendrez peut-être de meilleures performances en créant une animation CSS et en l'utilisant à la place. De même, si votre application s'appuie déjà sur JavaScript pour ses animations, il est préférable de respecter au moins la cohérence de votre codebase existant.

Si vous souhaitez examiner le code pour cet effet, consultez le dépôt GitHub d'exemples d'éléments d'interface utilisateur et, comme toujours, faites-nous savoir comment vous vous en sortez dans les commentaires ci-dessous.