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-functionqui obtient la fraction de temps qui sâest Ă©coulĂ©e (0au dĂ©but,1Ă la fin) et renvoie la fin de lâanimation (commeysur 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=0indique 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.
Commentaires
<code>, pour plusieurs lignes â enveloppez-les avec la balise<pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepenâŠ)