CSS3, Courbes de Bezier et Animations



test Box



CSS 3

CSS3 fournit les nouveaux attribut transition qui permettent de se passer de javascript pour les effets d'animations. Les animations sont basées sur des courbes de Bézier avec les paramétrages suivants pour les points de contrôle de la courbe:

TransitionPoints de contrôleFonction équivalente
linearP1(0.0, 0.0) et P2(1.0, 1.0)f(x) = x
easeP1(0.25, 0.1) et P2(0.25, 1.0) 
easeInP1(0.42, 0.0) et P2(1.0, 1.0)f(x) = 1 - cos(x * π/2)
easeOutP1(0.0, 0.0) et P2(0.58, 1.0)f(x) = sin(x * π/2)
easeInOutP1(0.42, 0.0) et P2(0.58, 1.0)f(x) = sin²(x * π/2)
backP1(0.4, 1.6) et P2(0.6, 1.0)Effet fournit par Robert Penner
mais pas par les transitions par défaut de CSS3.

Le point de départ de la courbe est toujours P0(0,0), le point d'arrivée de la courbe est toujours P3(1,1).

Différence d'implémentation entre Webkit et les bibliothèques javascript

Le code javascript fournit par Christian Effenberger reprend l'implémentation des courbes de Bézier de webkit. Dans la fonction CubicBezierAtTime le paramètre t repésente un point temporel sur la courbe. Une interpolation est alors effectuée avec la méthode de Newton pour retrouver les coordonnées (x,y).

Dans les bibliothèques javascript Mootools, jQuery, etc. le paramètre t correspond à l'abscisse (x) d'un point de la courbe. Ces bibliothèques utilisent les fonctions fournies par Robert Penner. Ces fonctions fournissent un plus grand nombre d'effet que les courbes de Bezier, par contre elles sont peu paramétrables.

Manipulations de base sur les fonctions

Les fonctions utilisées ici ont toutes les mêmes propriétés de base. Elles sont définies sur l'interval [0 1] et retourne un résultat dans l'interval [0 1], de plus f(0)=0 et f(1)=1.

Cela leur confèrent quelques propriétés intéressantes.
Par exemple, les effets In et Out se déduisent l'un de l'autre par modification des valeurs d'entrées et de sorties. Si Out(x) est une fonction codant un effet Out, alors In(x) = 1 - Out(1-x) sera la fonction codant l'effet In ( telles qu'elles sont définies dans la norme CSS3 et les différentes bibliothèques d'effets disponible).

De même, l'effet InOut s'écrit directement par transformation des paramètres:
InOut(x) = ((t*=2)<1? In(t) : 1+ Out(--t)) / 2, ou bien grâce à une fonction générique ad hoc, celle donnée ci-dessous permet en plus de choisir le point d'inflexion r:

var fInOut = function(func, x, r) {
  var r = arguments[2]? arguments[2] : 0.5 ;
  if ( r==0 || r>=1 ) throw new Error("finout: r must be greater than 0 and less than 1.");
  return r + ( x<r? -func(1-x/r)*r : func( (x-r)/(1-r) )*(1-r) );
  /* ou bien : return ((x*=2)<1? 1-func(1-x) : 1+func(--x))/2 ; /* dans le cas r==1/2 */	
}
/* usage : Y = fInOut( funcOut, x, r) , avec funcOut une référence sur une fonction */

Il est aussi possible de juxtaposer plusieurs fonctions différentes, ou de découper la fonction sur des intervalles contigüs. Le principe est alors le suivant:
Soit f1(x), f2(x), etc. des fonctions variant sur [0 1], avec x dans [0 1] et soit r1, r2 ... rn dans [0 1].
Il suffit de faire ( à condition que fn(rn) soit différent de 1 ) :

	if ( x < r1 ) return f1(x) ;
	else if ( x < r2 ) { t = f1(r1) ; x = (x-r1)/(r2-r1); return (1-t) * f2(x) + t ; }
	else if ( x < r3 ) { t = f2(r2) ; x = (x-r2)/(r3-r2); return (1-t) * f3(x) + t ; }
	...
	else { x = (x-rn)/(1-rn); t = fm(rn); return (1-t) * fn(x) + t ; }

C'est le cas de la fonction elastic fournit par Robert Penner.

Bibliothèque de fonctions d'animation

Il suffit d'écrire ces fonctions pour l'effet Out et ensuite leur appliquer les transformations vues au chapitre précédant. Dans toutes ces fonctions le paramètre x correspond au temps écoulé depuis le début de l'animation, divisé par la durée totale de l'animation (pour rester dans l'intervale [0 1]).

Les fonctions bounce et elastic diffèrent de celle de Robert Penner, mais elles sont plus simple à programmer, l'effet visuel est sensiblement le même, et de plus elles sont paramétrables.

Ci-dessous les fonctions primitives d'animation codent l'effet Out.

 
var map={
	'linear': function(x) { return x },
	'ease': function(x) { return Math.sin(x*Math.PI/2) },
	'cubic':function(x,n) { return 1-Math.pow(1-x,(typeof n=='number'?n:3)) },
	'circ': function(x) { return Math.sqrt(1-(x=1-x)*x) },
	'expo': function(x) { return 1-Math.pow(2, -10*x) },
	'back': function(x,s) { 
		s = ( typeof s == 'number' && s!=0 )? s : 1.70158; 
		return 1+((s+1)*(x=x-1)+s)*x*x },
	'bounce': function(x,a,p) { 
		a = ( typeof a == 'number' && a!=0 )? a : 4; // amplitude des rebonds  
		p = ( typeof p == 'number' && p!=0 )? p : 3; // période : nombre de rebonds 
		return Math.pow(x, Math.abs(Math.cos(x*p*Math.PI))/a) },
	'elastic': function(x,a,p) { 
		a= ( typeof a == 'number' && a!=0 )? a : 16 ; // amplitude des oscillations  
		p= ( typeof p == 'number' && p!=0 )? p :  8 ; // période : nombre d'oscillation  
		return Math.pow(x, Math.cos(x*p*Math.PI)/a) },
}

A partir de cette base on peut écrire une bibliothèque étendue prenant en charge les effets In, Out, InOut, etc. Soit en utilisant des fonctions ad hoc pour les effets In, InOut, etc. soit en les codant directement à partir des fonctions primitives. ( C'est ce qui est fait dans la bibliothèque easing de jQuery. )

var easingIn = function(f, x, a, p ) { return 1-f(1-x, a, p) };
var easingInOut = function(f, x, a, p ) { return ((x*=2)<1? 1-f(1-x,a,p) : 1+f(--x,a,p))/2 } ;
var easingOutIn = function(f, x, a, p ) {return ((x*=2)<1? f(x,a,p): 2-f(2-x,a,p) ) /2 };
// paramétrique InOut, r permet de décaler le point d inflexion
var easingInOutP = function(r, f, x, a, p) { var t = f(r,a,p) ;
	return ( (x<r || r>=1) ? f(x, a, p) : (1-t) * f( (x-r)/(1-r), a, p ) + t ) }

var easing = {
'linear': function(x,a,p) { return x },

// 'ease' 
'easeOut': function(x,a,p) { return Math.sin(x*Math.PI/2) },
'easeIn' : function(x,a,p) { return 1-Math.sin((1-x)*Math.PI/2) },
'easeInOut' : function(x,a,p) { return (y=Math.sin(x*Math.PI/2))*y },
'easeOutIn': function(x,a,p) { return easingOutIn( map.easeOut, x, a, p ) },
...
// 'back': 
'backOut'   : function(x,a) { 
	var s = ( a && typeof a== 'number' && a!=0 )? a : 1.70158; 
	return 1+((s+1)*(x=x-1)+s)*x*x },
'backIn' : function(x,a) { 
	var s = ( a && typeof a== 'number' && a!=0 )? a : 1.70158; 
	return ((s+1)*x-s)*x*x },
'backInOut': function(x,a,p) { return easingInOut( map.backOut, x, a, p ) },
'backOutIn': function(x,a,p) { return easingOutIn( map.backOut, x, a, p ) },

//'bounce': 
'bounceOut' : function(x,a,p) {
	a = ( a && typeof a == 'number' && a!=0 )? a : 4; // amplitude des rebonds 
	p = ( p && typeof p == 'number' && p!=0 )? p : 3; // nombre de rebonds 
	return Math.pow(x, Math.abs(Math.cos(x*p*Math.PI))/a) },
'bounceIn' : function(x,a,p) { return easeIn(map.bouncOut,x,a,p) }
'bounceInOut' : function(x,a,p) { return easeInOut(map.bouncOut,x,a,p) }
'bounceOutIn' : function(x,a,p) { return easeOutIn(map.bouncOut,x,a,p) }
...
}

Dans le cas de javascript il est aussi possible de coder les primitives et les effets In, Out, InOut, etc. directement dans l'objet Math. On peut alors écrire y = Math.bounce.InOut(t, 10, 3 ). Par exemple :

var methods={
	'Out' : function(t,a,p) { return this(t,a,p ) },
	'In'  : function(t,a,p) { return 1-this(1-t,a,p) },
	'InOut':function(t,a,p) { return ((t*=2)<1? 1-this(1-t,a,p) : 1+this(--t,a,p))/2 },
	'OutIn':function(t,a,p) { return ((t*=2)<1? this(t,a,p): 2-this(2-t,a,p) ) /2 },
}

var easing= new Object() ;

for(var i in map) { // Use closure to preserve 'func' and 'ease'
	if ( !map.hasOwnProperty(i) ) continue ;
	(function(func) {
		easing[i]=func ;
		for(var j in methods) {
			if ( !methods.hasOwnProperty(j) ) continue;
			(function( ease ) { // Use 'apply' to execute func.ease
				easing[i][j] = function() { return ease.apply(func, arguments); };
			})(methods[j]);
		}
	})(map[i]);
}

// Implement to Math Object !
for(var i in easing) if ( easing.hasOwnProperty(i) ) Math[i]=easing[i] ; 

/* usage : Y = Math.bounce.InOut( t, 5, 3) */

Le code ci-dessus mérite quelques explications. D'abord, dans l'objet method, this correspond à une des fonctions primitives liée lors de l'appel de la méthode. Ensuite les boucles imbriquées utilisent des closures pour préserver les valeurs de func et ease, sinon lors de l'appel c'est la dernière primitive et la dernière méthode qui seront invoquées. ( On aurait pu aussi utiliser, à la place des closures les instructions let func=mappy[i] et let ease=methods[j] ). Au final on se retrouve avec l'objet Math qui contient des méthodes telles que Math.bounce.Out = function() { methods.Out.apply( map.bounce, arguments ) }. Ici l'usage de apply est nécessaire sinon on a toutes les chances d'avoir this égal à window dans les méthodes Out, In, InOut, etc..

Si l'on a une fonction each pour les Objets, similaire à forEach pour les tableaux, le code se simplifie, les closures étant réalisée de manière transparente. C'est ce qui est fait dans cette page.

var easing = new Object() ;
map.each( function( func, i, m){
	easing[i]=func ;
	methods.each( function( ease, j, me) {
		easing[i][j] = function(){return ease.apply(func, arguments)};
		} );
	});

// Implement to Math Object !
easing.each( function(v,i,e) { Math[i]=v } );

/* usage : Y = methods.bounce.InOut(t, 5, 3) */

Construire une animation se fait alors très simplement; la nouvelle valeur se calculant par la formule:
value = from + ( to - from ) * Y,
avec from et to les valeurs de départ et d'arrivée voulues, et Y représentant la valeur retournée par une des fonctions d'animation .

«Animate» une fonction d'animation

La fonction

La fonction «animate» décrite ici permet de manipuler simplement n'importe quel objet du DOM. Cette fonction prend en paramètre une référence sur un noeud du DOM, une référence sur la méthode d'animation et un objet contenant les options de l'animation.


Options d'animation
OptionsDescriptionValeur
fpsLe nombre de frame par seconde50
durationLa durée de l'animation en micro seconde1000
methodUne référence sur une fonction d'animationlinear
argsUn tableau de paramètres éventuels pour «method»[]
callbackLa fonction de rappel pour réaliser l'animationvoid()
...Toutes les autres options sont envoyées à la fonction de rappel 

L'option method est une référence sur une fonction d'animation. Les fonctions d'animation acceptent deux paramètres, t qui est le pourcentage de temps écoulé depuis le début de l'animation, et args qui est un tableau contenant des paramètres supplémentaires (celui-ci est facultatif). Elles sont croissantes et retournent un nombre compris entre 0 et 1. Par exemple la fonction easeOut s'écrit :
function easeOut(t, duration) { return Math.sin( t/duration * Math.PI/2); }

L'option callback est une référence sur une fonction de rappel appelée fps fois par seconde par animate. Elle prend trois paramètres en arguments. Le premier argument est une référence sur le noeud DOM à manipuler, le deuxième est le nombre compris entre 0 et 1 retourné par method et le troisième est l'objet options passé comme second argument de animate.

Les primitives

Ici les fonction primitives et les méthodes In, Out, etc. sont rattachées à l'objet Math. De plus des primitives utilisant des Courbes de Bezier ont été ajoutées.

!! copier le code Ici !!

Exemple d'utilisation

Programmons un effet «Fade In/Out» pour le panneau vert ci-dessous. Pour cela, donnons lui un id "panneauVert". Il nous faut aussi un button pour déclencher l'effet. L'effet sera affecté à l'évennement onclick. Le code pour le bouton:

<button onclick="fadeInOut($('panneauVert'));">Fade</button>

( $ est un alias pour document.getElementById )
Il nous faut programmer la fonction fadeInOut qui servira de déclencheur pour l'animation. Ici la fonction de callback est une fonction volatile en raison de sa simplicité, mais elle sera rarement plus compliquée.

function fadeInOut(obj) { 
	if ( obj.style.opacity == 0 ) 
		animate( obj, Math.easeOut, { 
			callback: function(o,y,opt){ o.style.opacity=y; } } );
	else if ( obj.style.opacity == 1 ) 
		animate( obj, Math.easeIn, { 
			callback: function(o,y,opt){ o.style.opacity=1-y;} } );
}		

Les méthodes easeOut et easeIn correspondent aux fonctions définies par CSS3.

... Fade In Out Demo ...

Effet Kwick avec animate

L'effet Kwick est aussi simple à programmer. Mais cette fois nous aurons besoin des options dans la fonction de callback. Celle-ci devra re-calculer la taille de tous les éléments à la volée, les éléments nécessaires au calculs seront donc dans les options. D'abord une référence self sur la cible elle-même pour pouvoir récupérer sa taille courante. Ensuite la taille de départ et d'arrivée de la cible dans from et to. Enfin la taille du container dans maxSize.

/* initialisation de l'effet Kwick */

var items = $('kwick').getElementsByTagName('li');
var maxSize = parseInt( $style($('kwick')).width) ; 
for(var i=0; i<items.length; i++ ) {
	items[i].addEventListener('mouseover', function(e) { /* event handler */
		animate( items , {
			'method': Math.ease.Out,  
			'callback' : kwicky, 
			'self': e.target, 
			'from': parseInt($style(e.target).width), /* taille initial de self */
			'to': 300, 				  /* taille désirée au final */
			'maxSize': maxSize,                       /* taille max du container */
			'fps': 30 } ) }, true);
	items[i].addEventListener('mouseout', function(e) { /* event handler */
		animate( items , {
			'method': Math.back.Out, 
			'to': Math.floor( maxSize/items.length ), 
			 /* le reste est identique */
			 ... } ) }, true);
}

La fonction de callback kwicky se débrouille pour le mieux avec toutes les données fournies et modifie la largeur des éléments en conséquence. Notez que l'on aurait pu fournir dans les options l'attribut à modifier width, height, top, etc.

  •  Rouge
  •  Orange
  •  Jaune
  •  Vert
  •  Bleu