Dans ma série d'article sur les animations performantes, j'ai mis l'accent sur le fait qu'il était important de rester un maximum sur les propriétés CSS transform et opacity pour réaliser des animations fluides.

Mais en vrai, c'est compliqué. En effet, en se limitant au niveau des propriétés CSS, on perd malheureusement une grande partie de ce que nous permet de faire le CSS.

Il existe cependant une technique qui permet d'inverser le problème et nous délivre de toutes les étapes manuelles qu'il faudrait faire pour n'utiliser que transform et opacity. Il s'agit de FLIP, présentée au monde par Paul Lewis (a.k.a. @aerotwist) dans un des ses articles.

Dans la suite de cet article, je vais essentiellement réexpliquer ce qui est déjà présenté par Paul Lewis. Cela dit, je me concentrerai moins sur la partie code et approfondirai les cas d'utilisation.

C'est quoi FLIP ?

L'idée qu'il y a derrière cette technique est de calculer automatiquement les transformations. Pour ce faire, il faut passer par 4 étapes :

  1. First : on enregistre la position de départ de l'élément (ex : {top: 100, left: 50})
  2. Last : on place l'élément dans sa position finale et on enregistre sa position (ex : {top: 130, left: 50})
  3. Invert : on calcule la différence entre les deux positions (ex : {top: -30, left: 0}) et on l'applique à l'élément. Ainsi, quand on applique cette transformation à l'élément, c'est comme s'il était revenu à sa position initiale.
  4. Play : on lance l'animation en faisant diminuer petit à petit la différence. L'animation peut être gérée en JS (ce que j'ai fait ci dessous), mais plutôt que requestAnimationFrame on pourrait se contenter de transitions CSS.

Cela ressemble donc à ça :

// Récupération de l'élément
const element = document.getElementById('flip');

// Récupération de la position de départ (First)
const first = element.getBoundingClientRect();

// Mise à jour de l'élement dans sa position finale
// La mise à jour peut se faire depuis n'importe où.
// Ca n'a pas besoin d'être une classe sur l'élement.
// Ca peut par exemple être la création d'un nouvel
// élément dans une liste.
element.classList.add('opened');

// Récupération de la position finale (Last)
const last = element.getBoundingClientRect();

// Calcul de la différence (Invert)
const invert = {
  top: first.top - last.top, // 100 - 130 = -30
  left: first.left - last.left // 50 - 50 = 0
};

// Mise à jour visuelle de l'élément pour qu'il
// soit dans la position initiale
element.style.transform = `translate(
    ${invert.left}px,
    ${invert.top}px
)`;

// Lancement de l'animation (Play)
let start = window.performance.now();
requestAnimationFrame(animate);

const duration = 300;
const animate = () => {
  // Calcul du pourcentage d'avancement de l'animation
  let progress = (window.performance.now() - start) / duration;
  let progress = Math.max(0, Math.min(1, progress));

  // Mise à jour visuelle
  // Si progress = 0.2
  // translate(0px, (-30 * (1 - 0.2))px)
  // <=> translate(0px, -24px)
  element.style.transform = `translate(
        ${invert.left * (1 - progress)}px,
        ${invert.top * (1 - progress)}px
    )`;

  if (progress === 1) {
    // Fin de l'animation
    element.style.transform = null;
  } else {
    // On passe à l'étape suivante de l'animation
    requestAnimationFrame(animate);
  }
};

La première fois que j'ai vu cette technique, je me suis demandé pourquoi il n'y avait pas de sauts visuels.

En effet, quand on récupère la valeur de last, on a bien l'élement qui est positionné là où il est censé être à la fin de l'animation. Le navigateur nous dit même qu'il est entrain de l'afficher à cet endroit vu que getBoundingClientRect nous renvoie les bonnes valeurs.

En fait, cela vient du fait que le Layout et le Painting sont deux étapes totalement décorrélées au du niveau navigateur. Ainsi, il batch les étapes de painting, ce qui fait que la mise à jour visuelle se fera, non pas à chaque fois que vous changez votre élément, mais dès que le navigateur estime qu'il est temps de le mettre à jour. C'est ce qui vous laisse le temps d'appliquer le invert qui fait croire visuellement que l'élément est toujours dans sa position initiale.

Par ailleurs, dans cet exemple de code, afin de limiter le nombre de choses nouvelles, j'ai limité la transformation à la position de l'élément. Cependant, il est tout à fait possible d'en rajouter en jouant sur scale (si la hauteur et largeur de l'élément animé bouge) ou opacity. (cf. la librairie de Paul Lewis)

Quel intérêt par rapport à des animations normales ?

Sans parler de la mise en place dans le code, on peut déjà se demander ce que ça apporte.

La première et essentielle des raisons est la performance. En effet, vu que tout le calcul de l'animation est fait avant son démarrage (F, L, I), l'animation même est très rapide, d'autant que l'animation résultante (P) est basée sur les techniques performante.

La deuxième est l'abstraction du déplacement. En effet, une fois la lib sous jacente créée, il n'y a plus besoin de savoir que l'élément s'est déplacé de 10 ou 100 pixels sur la droite. C'est calculé à partir des infos déjà présentes dans le navigateur.

Pour autant, même si c'est une technique qui apporte beaucoup de facilitées, ce n'est pas pour autant la solution miracle.

Quand l'utiliser ?

Comme à chaque fois, tout est question de contexte.

❌ Contenu et position statiques

Pas d'animation FLIP si vos animations n'ont pas de rapport avec le contenu que vous animez, vraisemblablement il sera plus simple de rester sur des animations CSS pures. Ca peut être un bouton qui grossit au survol, un élément qui apparaît de manière rigolote quand il débarque sur la page, etc.

✅ Changement de position

Les animations FLIP excellent lorsque le contenu change de place. Ce sont les animations dans lesquelles il n'y a pas de transformation en hauteur ou en largeur mais uniquement des déplacements. La librairie react-flip-move illustre très bien ce principe.

MaJ: D'ailleurs, j'ai découvert ce bout de doc dans Vue.js le lendemain de la publication de cet article ! Le composant transition y est implémenté avec les animations FLIP :)

⚠️ Changement de contenu

Cette fois ci, pas de réponse universelle.

En effet, on est limité dans le type d'animations à disposition. Et donc,rapidement, on va tordre ou faire faire des sauts au contenu. Concrètement, si vous appliquez une transformation FLIP, sans réfléchir, ça peut ressembler à ça :

La solution est de séparer les différents éléments et de faire un petit peu de mise en scène, en décalant certaines transitions par rapport à d'autres et en jouant sur l'opacité. En reprenant la démo au dessus, on pourrait faire quelque chose de ce style :

Si vous inspectez l'élément dans votre navigateur, vous verrez qu'en fait, le fond blanc n'est plus unique, mais respecte la structure suivante :

<div>
    <div style="background: white">
        <button>Bouton</button>
    </div>
    <div style="position: relative">
        <!-- Fond animé -->
        <div style="background: white; height: 40px"></div>
        <!-- Contenu animé -->
        <div style="position: absolute">
            Contenu
        </div>
    </div>
</div>

Pour que les éléments puissent être animés individuellement, on a recours à des positions absolute, des hauteurs définies en JavaScript, etc.

De fait, c'est compliqué parce qu'on est obligé de détourner ce que sait déjà bien faire un navigateur, et surtout, parce qu'il est difficile de sortir un pattern qui marchera dans n'importe quelle situation. Par contre, au niveau performance, c'est quasi imbattable.

Ainsi, quand c'est le contenu qui change et que vous vous demandez si vous pouvez utiliser FLIP, la réponse est : ça dépend de où vous souhaiter positionner le marqueur entre performance et maintenance. Ce n'est jamais facile à déterminer et généralement, je conseille de commencer par privilégier la maintenance puis de benchmarker pour s'adapter en conséquence.

Conclusion

Les animations FLIP ne sont pas une solution miracle. Par contre, c'est une technique qu'il est toujours bon d'avoir dans son sac.

Si vous voulez voir comment j'ai codé les exemples, c'est du React et c'est accessible sur github. Le code est un peu crado, mais je vous prépare un article plus complet en me concentrant sur l'implémentation en React :)

En attendant, si vous avez des questions ou si vous connaissez d'autres patterns performants pour réaliser des animations sur le web, n'hésitez pas à venir me voir sur Twitter : ce sera avec plaisir !


Sources complémentaires :