13 novembre 2022

Animations JavaScript

Les animations JavaScript peuvent gérer des choses que CSS ne peut pas gérer.

Par exemple, le dĂ©placement le long d’un chemin complexe, avec une fonction de temporisation diffĂ©rente des courbes de BĂ©zier, ou une animation sur un Ă©lĂ©ment canvas.

Utilisation de setInterval

Une animation peut ĂȘtre implĂ©mentĂ©e sous la forme d’une sĂ©quence d’images – gĂ©nĂ©ralement de petites modifications des propriĂ©tĂ©s HTML/CSS.

Par exemple, en changeant style.left de 0px Ă  100px, on dĂ©place l’élĂ©ment. Et si nous l’augmentons dans setInterval, en changeant de 2px avec un minuscule retard, comme 50 fois par seconde, alors cela semble fluide. C’est le mĂȘme principe qu’au cinĂ©ma : 24 images par seconde suffisent pour que l’image soit fluide.

Le pseudo-code peut ressembler Ă  ceci :

let timer = setInterval(function() {
  if (animation complete) clearInterval(timer);
  else increase style.left by 2px
}, 20); // changement de 2px toutes les 20ms, environ 50 images par seconde

Exemple plus complet de l’animation :

let start = Date.now(); // mémoriser l'heure de début

let timer = setInterval(function() {
  // combien de temps s'est écoulé depuis le début ?
  let timePassed = Date.now() - start;

  if (timePassed >= 2000) {
    clearInterval(timer); // terminer l'animation aprĂšs 2 secondes
    return;
  }

  // dessiner l'animation Ă  l'instant timePassed
  draw(timePassed);

}, 20);

// Ă  mesure que timePassed passe de 0 Ă  2000
// left obtient des valeurs de 0px Ă  400px
function draw(timePassed) {
  train.style.left = timePassed / 5 + 'px';
}

Cliquez pour la démo :

Résultat
index.html
<!DOCTYPE HTML>
<html>

<head>
  <style>
    #train {
      position: relative;
      cursor: pointer;
    }
  </style>
</head>

<body>

  <img id="train" src="https://js.cx/clipart/train.gif">


  <script>
    train.onclick = function() {
      let start = Date.now();

      let timer = setInterval(function() {
        let timePassed = Date.now() - start;

        train.style.left = timePassed / 5 + 'px';

        if (timePassed > 2000) clearInterval(timer);

      }, 20);
    }
  </script>


</body>

</html>

Utilisation de requestAnimationFrame

Imaginons que nous ayons plusieurs animations fonctionnant simultanément.

Si nous les exĂ©cutons sĂ©parĂ©ment, alors mĂȘme si chacune d’entre elles possĂšde setInterval(..., 20), le navigateur devra repeindre bien plus souvent que toutes les 20ms.

C’est parce qu’elles ont un temps de dĂ©part diffĂ©rent, donc “toutes les 20 ms” diffĂšre entre les diffĂ©rentes animations. Les intervalles ne sont pas alignĂ©s. Nous aurons donc plusieurs animations indĂ©pendantes dans un intervalle de 20ms.

En d’autres termes, ceci :

setInterval(function() {
  animate1();
  animate2();
  animate3();
}, 20)


Est plus léger que trois appels indépendants :

setInterval(animate1, 20); // animations indépendantes
setInterval(animate2, 20); // à différents endroits du script
setInterval(animate3, 20);

Ces redessinages indĂ©pendants doivent ĂȘtre regroupĂ©s, afin de faciliter le redessinage pour le navigateur et donc de rĂ©duire la charge du processeur et d’obtenir un aspect plus fluide.

Il y a une autre chose Ă  garder en tĂȘte. Parfois, le CPU est surchargĂ©, ou il y a d’autres raisons de redessiner moins souvent (comme lorsque l’onglet du navigateur est cachĂ©), donc nous ne devrions vraiment pas le lancer tous les 20ms.

Mais comment le savoir en JavaScript ? Il existe une spĂ©cification Animation timing qui fournit la fonction requestAnimationFrame. Elle rĂ©pond Ă  toutes ces questions et mĂȘme plus.

La syntaxe :

let requestId = requestAnimationFrame(callback)

Cela programme la fonction callback pour qu’elle s’exĂ©cute au moment le plus proche oĂč le navigateur veut faire une animation.

Si nous modifions des Ă©lĂ©ments dans callback, ils seront regroupĂ©s avec d’autres callbacks requestAnimationFrame et avec les animations CSS. Il y aura donc un seul recalcul de la gĂ©omĂ©trie et un seul repeint au lieu de plusieurs.

La valeur retournĂ©e requestId peut ĂȘtre utilisĂ©e pour annuler l’appel :

// annuler l'exécution programmée du callback
cancelAnimationFrame(requestId);

Le callback reçoit un argument – le temps Ă©coulĂ© depuis le dĂ©but du chargement de la page en microsecondes. Ce temps peut aussi ĂȘtre obtenu en appelant performance.now().

Habituellement, callback s’exĂ©cute trĂšs rapidement, Ă  moins que le CPU soit surchargĂ© ou que la batterie de l’ordinateur portable soit presque dĂ©chargĂ©e, ou qu’il y ait une autre raison.

Le code ci-dessous montre le temps entre les 10 premiĂšres exĂ©cutions de requestAnimationFrame. Habituellement, c’est 10-20ms :

<script>
  let prev = performance.now();
  let times = 0;

  requestAnimationFrame(function measure(time) {
    document.body.insertAdjacentHTML("beforeEnd", Math.floor(time - prev) + " ");
    prev = time;

    if (times++ < 10) requestAnimationFrame(measure);
  })
</script>

Animation structurée

Maintenant nous pouvons faire une fonction d’animation plus universelle basĂ©e sur requestAnimationFrame :

function animate({timing, draw, duration}) {

  let start = performance.now();

  requestAnimationFrame(function animate(time) {
    // timeFraction passe de 0 Ă  1
    let timeFraction = (time - start) / duration;
    if (timeFraction > 1) timeFraction = 1;

    // calculer l'état courant de l'animation
    let progress = timing(timeFraction)

    draw(progress); // dessinez-le

    if (timeFraction < 1) {
      requestAnimationFrame(animate);
    }

  });
}

La fonction animate accepte 3 paramĂštres qui dĂ©crivent essentiellement l’animation :

`duration``

DurĂ©e totale de l’animation. Par exemple, 1000.

timing(timeFraction)

Fonction de chronomĂ©trage, comme la propriĂ©tĂ© CSS transition-timing-function qui obtient la fraction de temps qui s’est Ă©coulĂ©e (0 au dĂ©but, 1 Ă  la fin) et renvoie la fin de l’animation (comme y sur la courbe de BĂ©zier).

Par exemple, une fonction linĂ©aire signifie que l’animation se dĂ©roule uniformĂ©ment avec la mĂȘme vitesse :

function linear(timeFraction) {
  return timeFraction;
}

Son graph :

C’est comme transition-timing-function : linear. Il existe d’autres variantes intĂ©ressantes prĂ©sentĂ©es ci-dessous.

draw(progress)

La fonction qui prend l’état final de l’animation et le dessine. La valeur progress=0 indique l’état de dĂ©but d’animation, et progress=1 – l’état de fin.

Il s’agit de la fonction qui dessine rĂ©ellement l’animation.

Elle peut dĂ©placer l’élĂ©ment :

function draw(progress) {
  train.style.left = progress + 'px';
}


Ou faire n’importe quoi d’autre, nous pouvons animer toute chose, de n’importe quelle maniùre.

Animons l’élĂ©ment width de 0 Ă  100% en utilisant notre fonction.

Cliquez sur l’élĂ©ment pour la dĂ©monstration :

Résultat
animate.js
index.html
function animate({duration, draw, timing}) {

  let start = performance.now();

  requestAnimationFrame(function animate(time) {
    let timeFraction = (time - start) / duration;
    if (timeFraction > 1) timeFraction = 1;

    let progress = timing(timeFraction)

    draw(progress);

    if (timeFraction < 1) {
      requestAnimationFrame(animate);
    }

  });
}
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <style>
    progress {
      width: 5%;
    }
  </style>
  <script src="animate.js"></script>
</head>

<body>


  <progress id="elem"></progress>

  <script>
    elem.onclick = function() {
      animate({
        duration: 1000,
        timing: function(timeFraction) {
          return timeFraction;
        },
        draw: function(progress) {
          elem.style.width = progress * 100 + '%';
        }
      });
    };
  </script>


</body>

</html>

Le code pour cela :

animate({
  duration: 1000,
  timing(timeFraction) {
    return timeFraction;
  },
  draw(progress) {
    elem.style.width = progress * 100 + '%';
  }
});

Contrairement Ă  l’animation CSS, nous pouvons crĂ©er ici n’importe quelle fonction de temporisation et n’importe quelle fonction draw (de dessinnage). La fonction de timing n’est pas limitĂ©e par les courbes de BĂ©zier. Et draw peut aller au-delĂ  des propriĂ©tĂ©s, crĂ©er de nouveaux Ă©lĂ©ments pour une animation de feu d’artifice ou autre.

Fonctions de temporisation

Nous avons vu la fonction de temporisation la plus simple, linéaire, ci-dessus.

Nous allons en voir d’autres. Nous allons essayer des animations de mouvements avec diffĂ©rentes fonctions de temporisation pour voir comment elles fonctionnent.

Puissance de n

Si nous voulons accĂ©lĂ©rer l’animation, nous pouvons utiliser progress Ă  la puissance n.

Par exemple, une courbe parabolique :

function quad(timeFraction) {
  return Math.pow(timeFraction, 2)
}

Le graph :

Voir en action (cliquer pour activer) :


Ou la courbe cubique ou encore un n plus grand. En augmentant la puissance, on accélÚre la vitesse.

Voici le graphique de progress Ă  la puissance 5 :

En action :

L’arc

Fonction :

function circ(timeFraction) {
  return 1 - Math.sin(Math.acos(timeFraction));
}

Le graph:

Back : tir à l’arc

Cette fonction effectue le “tir à l’arc” (bow shooting). On commence par “tirer la corde de l’arc”, puis on “tire”.

Contrairement aux fonctions prĂ©cĂ©dentes, elle dĂ©pend d’un paramĂštre supplĂ©mentaire x, le “coefficient d’élasticitĂ©â€. La distance de “traction de la corde de l’arc” est dĂ©finie par celui-ci.

Le code :

function back(x, timeFraction) {
  return Math.pow(timeFraction, 2) * ((x + 1) * timeFraction - x)
}

Le graph pour x = 1.5:

Pour l’animation, nous l’utilisons avec une valeur spĂ©cifique de x. Exemple pour x = 1.5 :

Bounce

Imaginez que nous lĂąchons une balle. Elle tombe, puis rebondit plusieurs fois et s’arrĂȘte.

La fonction bounce fait la mĂȘme chose, mais dans l’ordre inverse : Le “rebond” commence immĂ©diatement. Elle utilise quelques coefficients spĂ©ciaux pour cela :

function bounce(timeFraction) {
  for (let a = 0, b = 1; 1; a += b, b /= 2) {
    if (timeFraction >= (7 - 4 * a) / 11) {
      return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2)
    }
  }
}

En action :

Animation élastique

Une fonction â€œĂ©lastique” de plus qui accepte un paramĂštre supplĂ©mentaire x pour la “portĂ©e initiale”.

function elastic(x, timeFraction) {
  return Math.pow(2, 10 * (timeFraction - 1)) * Math.cos(20 * Math.PI * x / 3 * timeFraction)
}

Le graph pour x=1.5:

En action pour x=1.5 :

Reversal : ease*

Nous avons donc une collection de fonctions de temporisation. Leur application directe est appelĂ©e “easeIn”.

Parfois, nous avons besoin de montrer l’animation dans l’ordre inverse. C’est possible avec la transformation “easeOut”.

easeOut

Dans le mode “easeOut”, la fonction timing est placĂ©e dans un wrapper timingEaseOut :

timingEaseOut(timeFraction) = 1 - timing(1 - timeFraction)

En d’autres termes, nous avons une fonction de “transformation” makeEaseOut qui prend une fonction de temporisation “rĂ©guliĂšre” et renvoie le wrapper qui l’entoure :

// accepte une fonction de temporisation, renvoie la variante transformée
function makeEaseOut(timing) {
  return function(timeFraction) {
    return 1 - timing(1 - timeFraction);
  }
}

Par exemple, nous pouvons prendre la fonction bounce dĂ©crite ci-dessus et l’appliquer :

let bounceEaseOut = makeEaseOut(bounce);

Ainsi, le rebond ne sera pas au dĂ©but, mais Ă  la fin de l’animation. C’est encore mieux :

Résultat
style.css
index.html
#brick {
  width: 40px;
  height: 20px;
  background: #EE6B47;
  position: relative;
  cursor: pointer;
}

#path {
  outline: 1px solid #E8C48E;
  width: 540px;
  height: 20px;
}
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
  <script src="https://js.cx/libs/animate.js"></script>
</head>

<body>


  <div id="path">
    <div id="brick"></div>
  </div>

  <script>
    function makeEaseOut(timing) {
      return function(timeFraction) {
        return 1 - timing(1 - timeFraction);
      }
    }

    function bounce(timeFraction) {
      for (let a = 0, b = 1; 1; a += b, b /= 2) {
        if (timeFraction >= (7 - 4 * a) / 11) {
          return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2)
        }
      }
    }

    let bounceEaseOut = makeEaseOut(bounce);

    brick.onclick = function() {
      animate({
        duration: 3000,
        timing: bounceEaseOut,
        draw: function(progress) {
          brick.style.left = progress * 500 + 'px';
        }
      });
    };
  </script>


</body>

</html>

Ici, nous pouvons voir comment la transformation change le comportement de la fonction :

S’il y a un effet d’animation au dĂ©but, comme le rebondissement – il sera affichĂ© Ă  la fin.

Dans le graphique ci-dessus, le regular bounce a la couleur rouge, et le easeOut bounce est bleu.

  • Regular bounce – l’objet rebondit en bas, puis Ă  la fin saute brusquement en haut.
  • AprĂšs easeOut – il saute d’abord vers le haut, puis rebondit lĂ .

easeInOut

Nous pouvons Ă©galement montrer l’effet Ă  la fois au dĂ©but et Ă  la fin de l’animation. La transformation est appelĂ©e “easeInOut”.

Étant donnĂ© la fonction de temporisation, nous calculons l’état de l’animation comme suit :

if (timeFraction <= 0.5) { // premiÚre moitié de l'animation
  return timing(2 * timeFraction) / 2;
} else { // deuxiÚme moitié de l'animation
  return (2 - timing(2 * (1 - timeFraction))) / 2;
}

Le code du wrapper :

function makeEaseInOut(timing) {
  return function(timeFraction) {
    if (timeFraction < .5)
      return timing(2 * timeFraction) / 2;
    else
      return (2 - timing(2 * (1 - timeFraction))) / 2;
  }
}

bounceEaseInOut = makeEaseInOut(bounce);

En action, bounceEaseInOut :

Résultat
style.css
index.html
#brick {
  width: 40px;
  height: 20px;
  background: #EE6B47;
  position: relative;
  cursor: pointer;
}

#path {
  outline: 1px solid #E8C48E;
  width: 540px;
  height: 20px;
}
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
  <script src="https://js.cx/libs/animate.js"></script>
</head>

<body>


  <div id="path">
    <div id="brick"></div>
  </div>

  <script>
    function makeEaseInOut(timing) {
      return function(timeFraction) {
        if (timeFraction < .5)
          return timing(2 * timeFraction) / 2;
        else
          return (2 - timing(2 * (1 - timeFraction))) / 2;
      }
    }


    function bounce(timeFraction) {
      for (let a = 0, b = 1; 1; a += b, b /= 2) {
        if (timeFraction >= (7 - 4 * a) / 11) {
          return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2)
        }
      }
    }

    let bounceEaseInOut = makeEaseInOut(bounce);

    brick.onclick = function() {
      animate({
        duration: 3000,
        timing: bounceEaseInOut,
        draw: function(progress) {
          brick.style.left = progress * 500 + 'px';
        }
      });
    };
  </script>


</body>

</html>

La transformation “easeInOut” joint deux graphiques en un seul : easeIn (rĂ©gulier) pour la premiĂšre moitiĂ© de l’animation et easeOut (inversĂ©) – pour la deuxiĂšme moitiĂ©.

L’effet est clairement visible si l’on compare les graphiques de easeIn, easeOut et easeInOut de la fonction de temporisation circ :

  • Red est la variante rĂ©guliĂšre de circ (easeIn).
  • Green – easeOut.
  • Blue – easeInOut.

Comme nous pouvons le voir, le graphique de la premiĂšre moitiĂ© de l’animation est le easeIn attĂ©nuĂ©, et la seconde moitiĂ© est le easeOut attĂ©nuĂ©. Par consĂ©quent, l’animation commence et se termine avec le mĂȘme effet.

Un " draw " plus intéressant

Au lieu de dĂ©placer l’élĂ©ment, nous pouvons faire autre chose. Il suffit d’écrire le bon draw.

Voici la saisie animĂ©e du texte “rebondissant” :

Résultat
style.css
index.html
textarea {
  display: block;
  border: 1px solid #BBB;
  color: #444;
  font-size: 110%;
}

button {
  margin-top: 10px;
}
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
  <script src="https://js.cx/libs/animate.js"></script>
</head>

<body>


  <textarea id="textExample" rows="5" cols="60">He took his vorpal sword in hand:
Long time the manxome foe he sought—
So rested he by the Tumtum tree,
And stood awhile in thought.
  </textarea>

  <button onclick="animateText(textExample)">Run the animated typing!</button>

  <script>
    function animateText(textArea) {
      let text = textArea.value;
      let to = text.length,
        from = 0;

      animate({
        duration: 5000,
        timing: bounce,
        draw: function(progress) {
          let result = (to - from) * progress + from;
          textArea.value = text.slice(0, Math.ceil(result))
        }
      });
    }


    function bounce(timeFraction) {
      for (let a = 0, b = 1; 1; a += b, b /= 2) {
        if (timeFraction >= (7 - 4 * a) / 11) {
          return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2)
        }
      }
    }
  </script>


</body>

</html>

Résumé

Pour les animations que CSS ne peut pas bien gĂ©rer, ou celles qui nĂ©cessitent un contrĂŽle prĂ©cis, JavaScript peut aider. Les animations JavaScript doivent ĂȘtre implĂ©mentĂ©es via requestAnimationFrame. Cette mĂ©thode intĂ©grĂ©e permet de configurer une fonction callback Ă  exĂ©cuter lorsque le navigateur prĂ©pare un repeint. En gĂ©nĂ©ral, c’est trĂšs bientĂŽt, mais le moment exact dĂ©pend du navigateur.

Lorsqu’une page est en arriĂšre-plan, il n’y a pas de repeint du tout, donc la fonction de rappel ne sera pas exĂ©cutĂ©e : l’animation sera suspendue et ne consommera pas de ressources. C’est trĂšs bien.

Voici la fonction d’aide animate pour configurer la plupart des animations :

function animate({timing, draw, duration}) {

  let start = performance.now();

  requestAnimationFrame(function animate(time) {
    // timeFraction passe de 0 Ă  1
    let timeFraction = (time - start) / duration;
    if (timeFraction > 1) timeFraction = 1;

    // calculer l'état courant de l'animation
    let progress = timing(timeFraction);

    draw(progress); // dessinez-le

    if (timeFraction < 1) {
      requestAnimationFrame(animate);
    }

  });
}

Options :

  • duration – la durĂ©e totale de l’animation en ms.
  • timing – la fonction pour calculer la progression de l’animation. Donne une fraction de temps de 0 Ă  1, retourne la progression de l’animation, gĂ©nĂ©ralement de 0 Ă  1.
  • draw – la fonction pour dessiner l’animation.

Nous pourrions certainement l’amĂ©liorer, mais les animations JavaScript ne sont pas utilisĂ©es quotidiennement. Elles sont utilisĂ©es pour faire quelque chose d’intĂ©ressant et de non standard. Vous voudriez donc ajouter les fonctionnalitĂ©es dont vous avez besoin quand vous en avez besoin.

Les animations JavaScript peuvent utiliser n’importe quelle fonction de temporisation. Nous avons couvert beaucoup d’exemples et de transformations pour les rendre encore plus polyvalentes. Contrairement Ă  CSS, nous ne sommes pas limitĂ©s ici aux courbes de BĂ©zier.

Il en va de mĂȘme pour draw : nous pouvons animer n’importe quoi, pas seulement des propriĂ©tĂ©s CSS.

Exercices

importance: 5

Créez une balle rebondissante. Cliquez pour voir à quoi elle doit ressembler :

Open a sandbox for the task.

Pour rebondir, nous pouvons utiliser les propriĂ©tĂ©s CSS top et position:absolute pour la balle Ă  l’intĂ©rieur du champ avec position:relative.

La coordonnĂ©e du bas du champ est field.clientHeight. La propriĂ©tĂ© CSS top fait rĂ©fĂ©rence au bord supĂ©rieur de la balle. Elle doit donc aller de 0 Ă  field.clientHeight - ball.clientHeight, c’est-Ă -dire la position finale la plus basse du bord supĂ©rieur de la balle.

Pour obtenir l’effet de “rebond”, nous pouvons utiliser la fonction de timing bounce en mode easeOut.

Voici le code final de l’animation :

let to = field.clientHeight - ball.clientHeight;

animate({
  duration: 2000,
  timing: makeEaseOut(bounce),
  draw(progress) {
    ball.style.top = to * progress + 'px'
  }
});

Ouvrez la solution dans une sandbox.

importance: 5

Faites rebondir la balle vers la droite. Comme ceci :

Écrivez le code d’animation. La distance à gauche est de 100px.

Prenez la solution de la tùche précédente Animer la balle rebondissante comme source.

Dans la tĂąche Animer la balle rebondissante, nous n’avions qu’une seule propriĂ©tĂ© Ă  animer. Maintenant, nous avons besoin d’une supplĂ©mentaire : elem.style.left.

La coordonnĂ©e horizontale change selon une autre loi : elle ne “rebondit” pas, mais augmente progressivement en dĂ©plaçant la balle vers la droite.

Nous pouvons écrire un autre animate pour elle.

Comme fonction de temporisation, nous pourrions utiliser linear, mais quelque chose comme makeEaseOut(quad) semble bien mieux.

Le code :

let height = field.clientHeight - ball.clientHeight;
let width = 100;

// animer top (rebondissement)
animate({
  duration: 2000,
  timing: makeEaseOut(bounce),
  draw: function(progress) {
    ball.style.top = height * progress + 'px'
  }
});

// animer left (déplacement vers la droite)
animate({
  duration: 2000,
  timing: makeEaseOut(quad),
  draw: function(progress) {
    ball.style.left = width * progress + "px"
  }
});

Ouvrez la solution dans une sandbox.

Carte du tutoriel

Commentaires

lire ceci avant de commenter

  • Si vous avez des amĂ©liorations Ă  suggĂ©rer, merci de soumettre une issue GitHub ou une pull request au lieu de commenter.
  • Si vous ne comprenez pas quelque chose dans l'article, merci de prĂ©ciser.
  • Pour insĂ©rer quelques bouts de code, utilisez la balise <code>, pour plusieurs lignes – enveloppez-les avec la balise <pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepen
)