Optimiser ses animation Javascript/CSS

Contenu du snippet

Bonjour,

On trouve souvent sur le net des sites qui utilisent Javascript/CSS pour animer leur pages, surtout depuis CSS3 qui introduit de nouveaux attributs/fonctions qui permettent d'animer (notament "transform" : http://www.w3.org/TR/css-transforms-1/).
Bien trop souvent j'ai rencontré des codes Javascript mal conçus, mal optimisés qui résultent bien souvent en mouvements saccadés.
Je propose dans cet article une solution (parmi d'autres) simple et efficace, en 'js pur'.

INTRODUCTION

Tout d'abord je dois expliquer ce que je concidère comme script mal conçu ou mal optimisé. Pour ce faire, je vais montrer quelque exemples.

Premier exemple :
// But recherché par l'auteur : faire varier marginLeft et marginTop sur ses divs

var myDiv0 = document.getElementById("divID0");
var myDiv1 = document.getElementById("divID1");
var myDiv2 = document.getElementById("divID2");

var marginleft0 = -5;
var margintop0 = 0;
var marginleft1 = 0;
var margintop1 = 0;
var marginleft2 = 5;

var interval0 = setInterval(function(){ marginleft0 += 6; myDiv0.style.marginLeft = marginleft0 + "px"; if(marginleft0 > 13) stopInterval(interval0) }, 40)
var interval0h = setInterval(function(){ margintop0 -= 3; myDiv0.style.marginTop = margintop0 + "px"; if(margintop0 > 10) stopInterval(interval0h) }, 40)

var interval1 = setInterval(function(){ marginleft1 += 3; myDiv0.style.marginLeft = marginleft1 + "px"; if(marginleft1 > 13) stopInterval(interval1) }, 45)
var interval1h = setInterval(function(){ margintop1 += 6; myDiv0.style.marginTop = margintop1 + "px"; if(margintop1 > 22) stopInterval(interval1h) }, 45)

var interval2 = setInterval(function(){ marginleft2 -= 6; myDiv0.style.marginLeft = marginleft2 + "px"; if(marginleft2 > 13) stopInterval(interval2) }, 50)



Second exemple :
// But recherché par l'auteur : effectuer une translation et une rotation

var myDiv = document.getElementById("myDiv");

var rotation = 0;
var translationX = 0;

for(var i = 0; i < 2000; i+=100){
    setTimeout( function(){
        rotation+= 10;
        translationX -= 20;
        myDiv.style.transform = "rotate(" + rotation + "deg, " + translationX + ")"; }
    , i)
}


C'est exactement ce genre de codes que j'aimerais voir disparaître du net. Avec peu d'éléments et d'opérations, ce genre de code fonctionne sans poser de problème, mais dès qu'il s'agit d'un nombre plus important d'objets et mouvements, ça devient juste la folie.

Ce que je conseille à tout codeur de faire, c'est de définir des fonctions, ça lui permettra :
1. de s'y retrouver plus facilement dans son code;
2. de se rendre compte des endroits où il peut optimiser;
3. de pouvoir modifier le comportement de son script sans avoir à modifier les deux millions de variables définies au cours de son script;
4. de partager son code.

Dans la suite de l'article, je donne des pistes au javascripteur pour optimizer son code avec des fonctions générales (applicable dans de nombreux cas) et qui tournent facilement (low CPU usage).

NOM DES ATTRIBUTS CSS EN JAVASCRIPT

Les attributs CSS possèdent parfois des tirets alors qu'en javascript jamais : le tiret disparait et la lettre suivante devient une majuscule.
Par exemple "margin-left" devient "marginLeft".

Voici une fonction qui réécrit les attributs CSS en leur nom Javascript, au besoin :
function CSSAttributeToJavascriptName( attribute ){

 var correctAtIndex = attribute.indexOf("-");
 while(correctAtIndex != -1){ attribute = attribute.substring(0, correctAtIndex) + attribute[correctAtIndex+1].toUpperCase() + attribute.substring(correctAtIndex+2); correctAtIndex = attribute.indexOf("-"); }
 return attribute;

}


FAIRE VARIER UN ATTRIBUT CSS AU COURS DU TEMPS

Dans le cas où l'on veut faire varier un attribut CSS pendant un certain délai, par une certaine période, voici la fonction que je propose :

Les arguments : element, attribute, string_format, from_value, to_value, delay, stepduration, callback

- element : l'élément HTML concerné;
- attribute : le nom de l'attribut CSS concerné (peut être donné comme en CSS ("margin-left") ou comme en js ("marginLeft"));
- string_format : une chaine de caractère contenant "{value}" là où la valeur doit être insérée (exemple : "{value}px");
- from_value : valeur de départ (est appliquée directement);
- to_value : valeur finale;
- delay : la durée de l'animation;
- stepduration : la fréquence de rafraichissement;
- callback (optionel) : une fonction appelée à la fin de l'animation avec element comme argument.

function CSSAnimateOverTime( element, attribute, string_format, from_value, to_value, delay, stepduration, callback ){
 /*
 Change CSS "attribute"'s value from "from_value" to "to_value" during delay every specified stepduration.
 string_format must be a string containing "{value}" where the value need to be inserted - example : "translate({value}px)"
 callback will be called (with element as unique argument) once animation is done.
 */

 attribute = CSSAttributeToJavascriptName(attribute);
 element.style[attribute] = string_format.replace("{value}", from_value);
 if (delay - stepduration > 0) from_value += (to_value - from_value) * stepduration / delay;
 else from_value = to_value;

 if (delay > 0) setTimeout( function(){ CSSAnimateOverTime( element, attribute, string_format, from_value, to_value, delay - stepduration, stepduration, callback); }, stepduration );
 else if(callback) callback( element );

}


Exemples d'utilisation :

// On fait bouger une div de 100px verticalement sur une durée de 1s par période de 20ms. Une fois terminé, on change sa couleur de fond en bleu.

function callback( element ){ element.style.backgroundColor = "blue"; }

function start(){
    CSSAnimateOverTime( document.getElementById("myDiv"), "transform", "translate({value}px)", 0, 100, 1000, 20, callback);
}

start();

/* =============================== */
// Et si on veut lui faire faire des aller-retours verticalement en boucle, facile grâce à callback
function callbackA( element ){ CSSAnimateOverTime( element, "transform", "translate({value}px)", 0, 100, 1000, 20, callbackB) }
function callbackB( element ){ CSSAnimateOverTime( element, "transform", "translate({value}px)", 100, 0, 1000, 20, callbackA) }

function start(){
    callbackA( document.getElementById("myDiv") );
}

start(); // C'est partit pour les aller-retours !


FAIRE VARIER PLUSIEURS ATTRIBUTS CSS AU COURS DU TEMPS

Lorsque l'on veut faire varier plusieurs attributs à la même période, plutôt que de définir plusieurs "setInterval" ou lancer plusieurs "setTimeout" ou d'utiliser la fonction donnée au dessus (CSSAnimateOverTime) plusieur fois, il vaut mieux avoir une seule fonction qui est appelée à chaque interval. Votre navigateur - ou celui du visiteur sur votre site - vous remercira.

Pour ce faire, j'ai écris une autre fonction, similaire à la précédente mais dans laquelle on peut passer plusieurs attributs à faire varier.

Les arguments : element, attributes, string_formats, from_values, to_values, delays, stepduration, callback, intermediate_callback

- element : l'élément HTML concerné;
- attributes : la liste des noms d'attributs CSS à faire varier;
- string_formats : une liste de chaine de caractères, chaque chaine contenant "{value}" à l'endroit où la valeur doit être spécifiée pour chaque attribut respectif (exemple : "{value}px") OU une seule chaine de caractère si la même s'applique à chaque attribut;
- from_values : une liste contenant les valeurs de départ de chaque attribut respectif OU une seule valeur si chaque attribut commence avec la même valeur;
- to_values : une liste contenant les valeurs à atteindre OU une seule valeur si la valeur est pareille pour chaque attribut;
- delays : une liste contenant les durées que les animations doivent prendre pour chaque attribut respectif OU une seule valeur si la durée est la même pour chaque attribut;
- stepduration : la période à laquelle la fonction sera appelée;
- callback (optionel) : une fonction à laquelle sera passée comme argument l'élément HTML concerné une fois la dernière animation terminée;
- intermediate_callback (optionel) : une fonction à laquelle est passée l'élément HTML concerné suivi de l'attribut pour lequel l'animation est terminée.

Le code (j'ai défini une fonction nommée "_fill" que la fonction principale utilise) :
function _fill( object, length ){
 if(typeof(object) != "object") object = new Array(object);
 if(object.length == 1) for(var i = 1; i < length; i++) object.push( object[0] );
 return object;
}

function CSSAnimateOverTimeMultiple( element, attributes, string_formats, from_values, to_values, delays, stepduration, callback, intermediate_callback ){

 // Fill arrays to allow user to pass single value type instead of array containing that value attributes.length times.
 string_formats = _fill(string_formats, attributes.length); from_values = _fill(from_values, attributes.length); to_values = _fill(to_values, attributes.length); delays = _fill(delays, attributes.length);
 
 var deletions = new Array(); // Will contain every index at which delay is < 0
 for(var a = 0; a < attributes.length; a++){
  if(delays[a] < 0) deletions.push(a);
  else{
   attributes[a] = CSSAttributeToJavascriptName(attributes[a]); // Convert CSS attribute name to its javascript name if needed.
   element.style[attributes[a]] = string_formats[a].replace("{value}", from_values[a]);
   if(delays[a] - stepduration > 0) from_values[a] += (to_values[a] - from_values[a]) * stepduration / delays[a];
   else from_values[a] = to_values[a];
   delays[a] -= stepduration;
  }
 }

 for(var d = 0; d < deletions.length; d++){
  if(intermediate_callback) intermediate_callback( element, attributes[deletions[d]] );
  attributes.splice(deletions[d], 1); delays.splice(deletions[d], 1); string_formats.splice(deletions[d], 1); from_values.splice(deletions[d],1); to_values.splice(deletions[d],1);
 }
 
 if (delays.length) setTimeout( function(){ CSSAnimateOverTimeMultiple( element, attributes, string_formats, from_values, to_values, delays, stepduration, callback, intermediate_callback ); }, stepduration );
 else if(callback) callback(element);

}


Exemples d'utilisation :

// Translation d'un objet HTML en diagonale Nord-Ouest puis arrivé au bout on continue en diagonale Nord-Est en utilisant les marges (margin-top, margin-left)
function switch_vertical( elem, attr ){
    if(attr == "marginLeft"){ CSSAnimateOverTime(elem, "margin-left", "{value}px", -100, 0, 1000, 20); }
}

function start(){
    CSSAnimateOverTimeMultiple( document.getElementById("myDiv"), ["margin-top", "margin-left"], "{value}px", 0, [-200, -100], [2000, 1000], 20, null, switch_vertical );
}

start();

/* ============================================ */
// Changer la taille de police du texte et sa couleur en boucle
function callbackA( elem ){
    CSSAnimateOverTimeMultiple( elem, ["font-size", "color"], ["{value}px", "rgb({value}, 100, 20)"], [10, 20], [24, 255], 500, 20, callbackB );
}

function callbackB( elem ){
    CSSAnimateOverTimeMultiple( elem, ["font-size", "color"], ["{value}px", "rgb({value}, 100, 20)"], [24, 255], [10, 20], 500, 20, callbackA );
}

function start(){
    callbackA( document.getElementById("myDiv") );
}


OPTIMISATIONS AVEC L'ATTRIBUT TRANSFORM en 2D

On utilise souvent les fonctions "rotate", "translate", "skew" ou autre avec l'attribut CSS "transform". On en utilise même souvent plusieurs en même temps ...
Le navigateur utilise une matrice 3x3 pour toutes les transformations du plan. Il est très intéressant lorsque l'on utilise plusieur fonctions en même temps de résumer le tout en matrice, on peut y gagner beaucoup en rapidité/fluidité. Cette matrice peut être accédée via la fonction "matrix". Je ne vais pas prendre le temps d'expliquer comment fonctionne cette fonction car cela prend du temps et est compliqué à expliquer à un publique pas forcément mathématicien. Il existe bien des outils qui écrivent vos matrices pour vous et bien des articles qui en expliquent le fonctionnement.

Voici quelques liens utiles :
- explications des matrices en CSS :
http://www.useragentman.com/blog/2011/01/07/css3-matrix-transform-for-the-mathematically-challenged/
https://developer.mozilla.org/fr/docs/CSS/transform
http://dev.opera.com/articles/understanding-the-css-transforms-matrix/
- créer vos matrices sans calculs : http://www.useragentman.com/matrix/

Voici ma méthode pour animer un élément HTML en utilisant les matrices :

function CSSAnimateTransformMatrix( element, from_matrix, to_matrix, delay, callback ){

 element.style.transform = "matrix(" + from_matrix.join(",") + ")";
 var timeout = 30;
 if(delay - (timeout/1000) > 0){
  for(var i = 0; i < 6; i++){
   from_matrix[i] += (to_matrix[i] - from_matrix[i]) * timeout / (delay*1000);
  }
 }else from_matrix = to_matrix;
 if (delay > 0) setTimeout(function(){ CSSAnimateTransformMatrix( element, from_matrix, to_matrix, delay - (timeout/1000), callback ); }, timeout );
 else if(callback) callback();

}


ANIMATIONS NON LINEAIRES

Toutes les fonctions données au dessus appliquent une animation linéaire (la variation de la valeur de l'attribut est constante).
Utiliser des fonctions non linéaires rend les animations encore plus "vivantes", intéressants.

Je vous laisse découvrir les fonctions que j'ai écrite (vous les trouverez dans le fichier .js ci joint), elle sont commentées et expliquées.

POUR FINIR ...

Comme je l'ai déjà dit, les fonctions données ci dessus ne sont que des pistes. Chacune d'entre elle peut être modifiée, adaptée à des cas plus précis.
Par exemple pour animer la fonction d'un texte en faisant varier les valeurs RGB de l'attribut color, il suffit de réécrire la fonction CSSAnimateOverTime avec quelque petits changements pour pouvoir y passer 3 valeurs de départ et 3 valeurs finales.

J'ai écrit cet article car il y a beaucoup de sites qui utilisent ce genre d'animations sans faire attention au poids sur le processeur mais c'est pareil pour toute utilisation de setTimeout et setInterval, qui sont utilisées à toute les sauces.
Gardez juste en tête que lorsque vous avez plusieurs setTimeout et qu'il est possible de les mettre en un (surtout si il y a des opération graphiques dans les fonction appelées), mettez les en un.
Souvent, l'erreur d'avoir des setTimeout et setInterval dans tous les sens vient du fait que le code n'est pas structuré/décomposé correctement - n'hésitez pas à écrire 1 ligne en 3 et à décomposer 1 fonction en 36 autres.

A voir également

Vous n'êtes pas encore membre ?

inscrivez-vous, c'est gratuit et ça prend moins d'une minute !

Les membres obtiennent plus de réponses que les utilisateurs anonymes.

Le fait d'être membre vous permet d'avoir un suivi détaillé de vos demandes et codes sources.

Le fait d'être membre vous permet d'avoir des options supplémentaires.