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 :
<!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 (commey
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, etprogress=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 :
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 :
#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
:
#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” :
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.