var timerId ,
startTime = 0,
fps=100,
duration = 1000,
method = null,
currentEffect = method ;
/* Part of MOJO library */
function $(obj) {
if ( !document.getElementById && document.all ) document.getElementById=function(id) { return document.all[id];}
return (document.getElementById(obj) || obj );
}
function $style(obj /*, cssItem */) {
var style = ( obj.currentStyle || window.getComputedStyle(obj,null) );
return arguments[1] ? style[arguments[1]] : style ;
}
function addEvent( obj, evt, fn ) {
if ( obj.addEventListener ) obj.addEventListener(evt,fn,false);
else if ( obj.attachEvent ) obj.attachEvent('on'+evt,fn);
}
function removeEvent(obj, evt, fn) {
if ( obj.removeEventListener ) obj.removeEventListener(evt,fn,false);
else if ( obj.dettachEvent ) obj.dettachEvent('on'+evt,fn);
}
function stopEvent(e) { // e is an Event
if(e.preventDefault){e.preventDefault();e.stopPropagation();} // standard DOM code
else if(e.cancelBubble==false){e.cancelBubble=true; e.returnValue=false;} // ie
return false; // for the others
}
Object.prototype.toArray = function() {
for(var i=0, len=this.length, result=[]; i>> 0;
var thisp = arguments[1];
for(var i=0; i0 ) fps=Math.min(1000, options['fps']);
if ( options['duration'] ) duration = options['duration'];
if ( typeof options['method'] == 'function' ) method=options['method'];
if ( typeof options['args'] == 'object' ) args=options['args'];
if ( typeof options['callback'] == 'function' ) callback=options['callback'];
/* setting timer */
if( obj.timerId ) clearInterval(obj.timerId); /* clear previous timer */
obj.timerId = setInterval( function() {
var t = Date.now() - startTime ;
if ( t=this.Hy; y-= step ) {
moveTo( this.Ox, y);
lineTo( this.Ox-10, y);
}
closePath(); stroke(); restore();
}
}
this.scale = function(x,y) {
/* converti un point dans [0,1] dans le système de coordonnées du canvas */
return { 'x' : x * ( this.Wx - this.Ox ) + this.Ox,
'y' : y * ( this.Hy - this.Oy ) + this.Oy }
}
this.drawPoint = function( x, y) {
/* affiche un point à l'emplacement x,y, (x,y) dans [0,1] */
var p=this.scale(x,y);
with(this.ctx) { fillRect(p.x,p.y,1,1); }
}
this.drawCircle = function( x, y , radius /*, color */ ) {
/* affiche un cercle à l'emplacement x,y, (x,y) dans [0,1] */
var p = this.scale(x,y);
with( this.ctx) {
save(); if ( arguments[3] ) fillStyle = arguments[3] ;
beginPath() ; arc( p.x, p.y, radius, 0, 2* Math.PI, true); fill();
restore();
}
}
this.drawBezierPoint = function( px, py, qx, qy /*, color */) {
/* affiche les points de contrôles d'une courbe de Bézier */
color = arguments[4] ? arguments[4]:null;
this.drawCircle( px, py, 5 , color);
this.drawCircle( qx, qy, 5 , color);
this.lineTo( px, py, 0, 0 , color );
this.lineTo( qx, qy, 1, 1 , color );
}
this.lineTo = function ( sx, sy, ex, ey /*, color */) {
/* trace une ligne entre (sx,sy) et (ex,ey) dans le système de coordonnées du canvas */
var start = this.scale( sx, sy ) ;
var end = this.scale( ex, ey ) ;
with( this.ctx ) {
save(); if ( arguments[4] ) strokeStyle = arguments[4] ;
beginPath();
moveTo( start.x, start.y ) ;
lineTo( end.x, end.y ) ;
closePath(); stroke(); restore();
}
}
this.bezierCurveTo= function( px, py, qx, qy, x, y /*, color */ ) {
var p =this.scale(px,py), q=this.scale(qx,qy), o=this.scale(x,y) ;
with(this.ctx) {
save(); strokeStyle = this.curveColor;
if ( arguments[7] ) strokeStyle = arguments[7];
beginPath(); moveTo(this.Ox,this.Oy); bezierCurveTo(p.x, p.y, q.x, q.y, o.x, o.y); stroke();
restore();
}
}
/* context manipulation */
this.restore = function() { this.ctx.restore(); }
this.save = function() { this.ctx.save(); }
this.reset = function() {
this.ctx.fillStyle = this.fgColor;
this.ctx.strokeStyle = this.fgColor;
this.ctx.clearRect(0,0, this.canvas.width, this.canvas.height ) ;
this.setDrawingShape( 40, 20, this.canvas.width-30, this.canvas.height-40*2 );
this.drawXaxis(10);
this.drawYaxis(10);
this.drawGrid(10);
this.save();
}
this.canvas = document.getElementById( canvasId ) ;
this.ctx = ( this.canvas.getContext ) ? this.canvas.getContext('2d') : null ;
if ( this.ctx == null ) throw new TypeError('canvas is null') ;
this.setSize() ;
this.reset();
}
function reset( method) {
document.getElementById('out').innerHTML= '' ;
var p= { 'linear': [ 0, 0, 1, 1 ],
'ease' : [ 0.25, 0.1, 0.25, 1.0 ],
'easeIn': [ 0.42, 0, 1, 1 ],
'easeOut': [ 0, 0, 0.58, 1 ],
'easeInOut': [ 0.42, 0, 0.58, 1 ],
'skidOut': [ 0.4, 1.6, 0.6, 1.0 ],
'custom': [ 0.1, 0.5, 0.5, 1 ]
}
box = document.getElementById('box');
box.style.width = '100px';
canvas.reset();
if ( method && method.name ) {
var n=method.name ;
canvas.setTitle(method.name);
if ( !p[n] ) return ;
canvas.drawCircle( p[n][0], p[n][1], 5 , 'red') ;
canvas.drawCircle( p[n][2], p[n][3], 5 , 'red') ;
canvas.lineTo( p[n][0], p[n][1], 0, 0 , 'red') ;
canvas.lineTo( p[n][2], p[n][3], 1, 1 , 'red') ;
canvas.bezierCurveTo( p[n][0], p[n][1], p[n][2], p[n][3], 1, 1) ;
}
}
function start(method, duration) {
startTime = Date.now();
box = document.getElementById('box');
currentEffect = method ;
reset( method );
timerId = setInterval( function() {
var t = Date.now() - startTime ;
if ( t < duration ) y = method( t/duration,[0,0,1,1] );
else { y=1 ; clearInterval( timerId ); }
box.style.width = parseInt( 100 + y * 400 ) + 'px';
canvas.drawPoint(t/duration,y) ;
} , parseInt( 1000/fps ) );
}
/*
var easingInOut = function(f, x, args ) { return ((x*=2)<1? 1-f(1-x,args) : 1+f(--x,args))/2 } ;
var easingOutIn = function(f, x, args ) {return ((x*=2)<1? f(x,args): 2-f(2-x,args) ) /2 };
// paramétrique InOut function r permet de décaler le point d inflexion
var easingInOutP = function(r, f, x, args) { var t = f(r,args) ;
return ( (x=1) ? f(x, args) : (1-t) * f( (x-r)/(1-r), args ) + t ) }
*/
/*
var map = {
'linear': function(x,args) { return x },
//'ease': function(x,args) { return Math.sin(x*Math.PI/2) },
'easeOut': function(x,args) { return Math.sin(x*Math.PI/2) },
'easeIn' : function(x,args) { return 1-Math.sin((1-x)*Math.PI/2) },
'easeInOut' : function(x,args) { return (y=Math.sin(x*Math.PI/2))*y },
//'easeOutIn' : function(x,args) { return ((x*=2)<1? Math.sin(x*Math.PI/2): 2-Math.sin((2-x)*Math.PI/2) ) /2 },
'easeOutIn': function(x,args) { return easingOutIn( map.easeOut, x, args ) },
'quadOut' : function(x,args) { return 1-(x=1-x)*x },
'quadIn' : function(x,args) { return x*x },
'quadInOut': function(x,args) { return easingInOut( map.quadOut, x, args ) },
'quadOutIn': function(x,args) { return easingOutIn( map.quadOut, x, args ) },
'cubicOut' : function(x,args) { return 1-Math.pow(1-x,3) },
'cubicIn' : function(x,args) { return Math.pow(x,3) },
'cubicInOut': function(x,args) { return easingInOut( map.cubicOut, x, args ) },
'cubicOutIn': function(x,args) { return easingOutIn( map.cubicOut, x, args ) },
'quartOut' : function(x,args) { return 1-Math.pow(1-x,4) },
'quartIn' : function(x,args) { return Math.pow(x,4) },
'quartInOut': function(x,args) { return easingInOut( map.quartOut, x, args ) },
'quartOutIn': function(x,args) { return easingOutIn( map.quartOut, x, args ) },
'quintOut' : function(x,args) { return 1-Math.pow(1-x,5) },
'quintIn' : function(x,args) { return Math.pow(x,5) },
'quintInOut': function(x,args) { return easingInOut( map.quintOut, x, args ) },
'quintOutIn': function(x,args) { return easingOutIn( map.quintOut, x, args ) },
//'circ' : function(x) { return Math.sqrt(1-(x=1-x)*x) },
'circOut' : function(x) { return Math.sqrt(1-(x=1-x)*x) },
'circIn' : function(x) { return 1-Math.sqrt(1-x*x) },
'circInOut': function(x,args) { return easingInOut( map.circOut, x, args ) },
'circOutIn': function(x,args) { return easingOutIn( map.circOut, x, args ) },
//'expo' : function(x) { return 1-Math.pow(2, -10*x) },
'expoOut' : function(x) { return 1-Math.pow(2, -10*x) },
'expoIn' : function(x) { return Math.pow(2, 10*(x-1)) },
'expoInOut': function(x,args) { return easingInOut( map.expoOut, x, args ) },
'expoOutIn': function(x,args) { return easingOutIn( map.expoOut, x, args ) },
'backOut' : function(x,args) {
var s = ( args && typeof args[0] == 'number' && args[0]!=0 )? args[0] : 1.70158;
return 1+((s+1)*(x=x-1)+s)*x*x },
'backIn' : function(x,args) {
var s = ( args && typeof args[0] == 'number' && args[0]!=0 )? args[0] : 1.70158;
return ((s+1)*x-s)*x*x },
'backInOut': function(x,args) { return easingInOut( map.backOut, x, args ) },
'backOutIn': function(x,args) { return easingOutIn( map.backOut, x, args ) },
'elasticOut': function(x,args) {
var a= ( args && typeof args[0] == 'number' && args[0]!=0 )? args[0] : 16 ; // amplitude des oscillations
var p= ( args && typeof args[1] == 'number' && args[1]!=0 )? args[1] : 8 ; // période : nombre d'oscillation
return Math.pow(x, Math.cos(x*p*Math.PI)/a) },
'elasticIn': function(x,args) {
var a= ( args && typeof args[0] == 'number' && args[0]!=0 )? args[0] : 16 ; // amplitude des oscillations
var p= ( args && typeof args[1] == 'number' && args[1]!=0 )? args[1] : 8 ; // période : nombre d'oscillation
return 1-Math.pow((x=1-x), Math.cos(x*p*Math.PI)/a) },
'elasticInOut': function(x,args) { return easingInOut( map.elasticOut, x, args ) },
'elasticOutIn': function(x,args) { return easingOutIn( map.elasticOut, x, args ) },
'bounceOut' : function(x,args) {
var a = ( args && typeof args[0] == 'number' && args[0]!=0 )? args[0] : 4; // amplitude des rebonds
var p = ( args && typeof args[1] == 'number' && args[1]!=0 )? args[1] : 3; // période : nombre de rebonds
return Math.pow(x, Math.abs(Math.cos(x*p*Math.PI))/a) },
'bounceIn' : function(x,args) {
var a = ( args && typeof args[0] == 'number' && args[0]!=0 )? args[0] : 4; // amplitude des rebonds
var p = ( args && typeof args[1] == 'number' && args[1]!=0 )? args[1] : 3; // période : nombre de rebonds
return 1-Math.pow((x=1-x), Math.abs(Math.cos(x*p*Math.PI))/a) },
'bounceInOut': function(x,args) { return easingInOut( map.bounceOut, x, args ) },
'bounceOutIn': function(x,args) { return easingOutIn( map.bounceOut, x, args ) },
'CubicBezierLinear' : function(t,args) { return CubicBezierAtTime(t*1000, 0, 0, 1, 1, 1000)}, // or return (t/d)
'CubicBezierEase' : function(t,args) { return CubicBezierAtTime(t*1000, 0.25, 0.1, 0.25, 1.0, 1000) },
'CubicBezierEaseIn' : function(t,args) { return CubicBezierAtTime(t*1000, 0.42, 0.0, 1.0, 1.0, 1000) },
'CubicBezierEaseOut': function(t,args) { return CubicBezierAtTime(t*1000, 0.0, 0.0, 0.58, 1.0, 1000) },
'CubicBezierEaseInOut':function(t,args){ return CubicBezierAtTime(t*1000, 0.42, 0.0, 0.58, 1.0, 1000) },
'CubicBezierBackOut': function(t,args) { return CubicBezierAtTime(t*1000, 0.4, 1.6, 0.6, 1.0, 1000) },
}
*/
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) },
}
var methods={
//'None': function(t,args) { return this(t,args) },
'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]);
}
for(var i in easing) if ( easing.hasOwnProperty(i) ) Math[i]=easing[i] ; // Implement to Math 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)};
} );
});
easing.each( function(v,i,e) { Math[i]=v } );
/*
for(var i in map) { /* setting all to Math * /
// let f=map[i] ; // MUST use LET here !!
( function(f) { // or use a CLOSURE
Math[i] = f ;
Math[ i+'Out'] = f;
Math[ i+'In' ] = function(t,a,b) { return 1-f(1-t,a,b) } ;
Math[ i+'InOut'] = function(t,a,b) { return ((t*=2)<1? 1-f(1-t,a,b) : 1+f(--t,a,b))/2 };
Math[ i+'OutIn'] = function(t,a,b) { return ((t*=2)<1? f(t,a,b): 2-f(2-t,a,b) ) /2 };
})(map[i]);
}
*/
// for(var i in easing) Function.prototype[i] = easing[i]; /* setting easing to Function , there is a bug with that */
/* usage: y = Math.bounceInOut(t, args) */
/* Robert Penner function ... * /
function bounceOut(t,duration) { /* effet de rebond from Robert Penner * /
var bounce = function(x) {
var f = 2.75, f2=f*f /* 7.5625 * /;
if (x < (1/f) ) { return (f2*x*x) ; }
else if (x < (2/f)) { return (f2*(x-=(1.5/f))*x + .75); }
else if (x < (2.5/f)) { return (f2*(x-=(2.25/f))*x + .9375); }
else { return (f2*(x-=(2.625/f))*x + .984375); } /*2.625* /
}
return bounce(t/duration);
// l equation de base est P(x) = Q(x)²*f² sur les intervalles [0 1/f] [1/f 2/f] [2/f 2.5/f] [2.5/f 1]
// pour chaque intervalle on calcule Q(x) = x - (inf+sup)/2f pour centrer la parabole sur [inf/f sup/f]
// on ajoute ensuite un Delta égal à ( 1 - (sup-inf)² )
// on a f = 11/4 , et les bornes 4/11, 8/11, 10/11, 21/11 ce qui donne (4+8+10)/11 = 2 rebonds,
// la dernière borne est (3 * 11) - (4+8+10 )) / 11 = 21 / 11
}
function skid( t, duration ) { /* effet de dépassement from Robert Penner * /
var s = 1.70158 ; return (( s+1 ) * (t=t/duration-1) + s )*t*t + 1;
}
function bounce(t,duration) { /* Bounce paramétrable * /
var e = 1.5 , nb = 4 ;
var f = function(x) { x=x-0.5; return x*x*e }
var rn=(nb-1)/nb, p = f(1) ;
var x = t/duration;
if ( x < rn ) {
for(var i=1; i'+titles[i].innerHTML+'';
item.addEventListener('click', toggleToc, false );
list.appendChild(item);
}
/* add all to container */
toc.appendChild( button );
toc.appendChild( list ) ;
}
/* generic function */
function toggle(obj, content ) { /* show hide */
var o = $(obj) ;
if ( typeof content !== 'undefined' ) o.innerHTML = content ;
o.style.display = o.style.display=='none'?'block':'none';
return false;
}
window.onload = function() { canvas = new canvasFuncDraw( 'canvas') ; toc = setTableOfContent('toc','h2', 'Chapitres');}
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:
Transition
Points de contrôle
Fonction équivalente
linear
P1(0.0, 0.0) et P2(1.0, 1.0)
f(x) = x
ease
P1(0.25, 0.1) et P2(0.25, 1.0)
easeIn
P1(0.42, 0.0) et P2(1.0, 1.0)
f(x) = 1 - cos(x * π/2)
easeOut
P1(0.0, 0.0) et P2(0.58, 1.0)
f(x) = sin(x * π/2)
easeInOut
P1(0.42, 0.0) et P2(0.58, 1.0)
f(x) = sin²(x * π/2)
back
P1(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.
function animate( obj, options ) { /* animate a DOM Node */
var startTime = Date.now() ;
/* default options */
var fps = 50, duration = 1000,
method=function(t) { return t; /* linear */ },
args = [],
callback=function(o,y,opt) { /* void() */ } ;
/* setting options */
if ( options['fps'] && options['fps']>0 ) fps=Math.min(1000, options['fps']);
if ( options['duration'] ) duration = options['duration'];
if ( typeof options['method'] == 'function' ) method=options['method'];
if ( typeof options['args'] == 'object') args = options['args];
if ( typeof options['callback'] == 'function' ) callback=options['callback'];
/* setting timer */
if ( obj.timerId ) clearInterval( obj.timerId );
obj.timerId = setInterval( function() {
var t = Date.now() - startTime ;
if ( t<duration ) callback(obj, method(t/duration,args), options );
else { clearInterval(obj.timerId); callback(obj,1,options) }
}, parseInt( 1000/fps ) );
}
Options d'animation
Options
Description
Valeur
fps
Le nombre de frame par seconde
50
duration
La durée de l'animation en micro seconde
1000
method
Une référence sur une fonction d'animation
linear
args
Un tableau de paramètres éventuels pour «method»
[]
callback
La fonction de rappel pour réaliser l'animation
void()
...
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:
( $ 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.
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.