25 de Octubre de 2014

Notas Espacio Programación

galería de imágenesvideo
AUTOR: Andrés Fernández
FECHA: 9/3/2009
LECTURAS:52806
Buscar Notas
volver
Cómo realizar un buen efecto de transición en Javascript

Transiciones en Javascript

Veremos cuáles son los errores más frecuentes en los que podemos incurrir al crear nuestras animaciones javascript y cómo evitarlos.
Es probable que luego de dar nuestros primeros pasos en javascript, un día conozcamos los temporizadores (setTimeout y setInterval) y creemos nuestras primeras animaciones. Seguramente nos sentiremos emocionados al ver una imagen o una capa moviéndose por la pantalla... pero ¿habremos hecho nuestra animación de la mejor manera?, ¿se verá igual en todos los navegadores? Estas son las preguntas que nos haremos seguramente, y a estas seguirán otras: ¿mi animación siempre avanza a la misma velocidad, cómo harán otros para lograr movimientos más suavizados o esos magníficos efectos de elasticidad?.

El error más típico que podemos encontrar en las animaciones (sobre todo en códigos de novatos o en códigos antiguos) es el uso de retardos constantes. ¿Por qué es un error? Como el tiempo de proceso varía según el equipo y el navegador que usemos, nuestra animación se verá muy distinta en diferentes equipos o en diferentes navegadores.

El segundo error típico es el uso de velocidades constantes (movimiento rectilíneo uniforme). ¿Por qué es un error? Porque ante cualquier pico de consumo de CPU serán mucho más evidentes los saltos en nuestra animación (pequeñas interrupciones), que en movimientos no uniformes.

Nota: Estas microinterrupciones ocurren siempre, e incluso, midiendo los aplazamientos que producen en un proceso contínuo de retraso constante, pueden aprovecharse para calcular el consumo de CPU: JPU.
Pero repetimos, lamentablemente no hay manera de evitar esas microinterrupciones y sólo nos queda disimularlas de la mejor manera posible.



Veamos un ejemplo típico de animación que tenga estos 2 problemas:



El código utilizado es el siguiente:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>test</title>
<style>
*{margin:0}
body{ background-color:#000; font-family:Verdana, Arial, Helvetica, sans-serif; font-size:9px}
#cuadro{width:50px; height:50px; background-color:#F00; position:absolute; text-align:center; line-height:50px; color:white; margin-top:85px; cursor:pointer}
</style>
<script>
var 
p=0.01,t;
function 
mover(obj){
if(
p>350){
clearTimeout(t);
return;
}
p+=5;
obj.style.left=p+'px';
t=setTimeout( function(){ mover(obj) },30 ); }
</
script>  
</head>
<body>
<div id="cuadro" onclick="mover(this)">click</div>
</body>
</html>



El código es correcto, incluso está muy bien para enseñar cómo funciona un temporizador, pero posee los 2 problemas señalados anteriormente: el retardo es constante (30 milisegundos) y el movimiento es uniforme (velocidad=5px/30ms=k).
Es muy probable que no se note ningún salto en este ejemplo ya que el mismo es muy breve y sólo modifica una única propiedad css, pero en animaciones más largas o que involucren más propiedades css, y sobre todo en equipos antiguos, seguramente notaríamos alguna deficiencia.

¿Cómo mejorar esto?

Una manera de mejorar esto (aunque todavía no la más óptima) es cambiar el valor de retardo en cada avance de fotograma de nuestra animación. Veamos el siguiente ejemplo:



El código de ese ejemplo es el siguiente:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>test</title>
<style>
*{margin:0}
body{ background-color:#333;margin:10px 2px}
#im{-khtml-opacity:0; -moz-opacity:0; filter:alpha(opacity=0); opacity:0;zoom:1; margin-top:2px}
input{ background-color:#CCC; font-family:Verdana, Arial, Helvetica, sans-serif; font-size:10px; color:#F00; border:1px solid #000}
</style>
<script>
function 
setOp(objvalue) {
    
obj.style.opacity value/100;
    
obj.style.MozOpacity value/100;
    
obj.style.KhtmlOpacity value/100;
    
obj.style.filter 'alpha(opacity=' value')';
    
obj.style.zoom=1;//necesario para Explorer

function 
efecto(o){
    for (var 
c=100c++) 
        (function(
c){
             
setTimeout(function(){setOp(o,c);} , 10*c);
        })(
c);

</
script>  
</head>

<body>
<form id="form1" name="form1" method="post" action="">
  <input type="button" name="Submit" value="iniciar" onclick="efecto(document.getElementById('im'))" />
</form>
<img id="im" src="http://www.disegnocentell.com.ar/ejemplos/img/27.jpg" width="400" />
</body>
</html>



Una simple ojeada a ese código nos muestra que la secuencia de retardos es: 0,10,20,...,990. Es decir, los retardos no son constantes. No obstante esto, no nos libramos totalmente de los "saltos" en el caso en que el navegador esté ocupado ni tampoco nos libramos de que se vea diferente en distintos equipos y/o navegadores. Además, el rendimiento es inversamente proporcional a la cantidad de iteraciones que realicemos para lograr el efecto.

¿Entonces...?

Entonces, el camino es aplicar el concepto que se utiliza en las animaciones modernas, en el cual los cálculos que definen cada actualización no excluyen los tiempos de proceso.

¿Cómo se logra eso?

1) Se define la duración total del efecto y se guarda en la variable ms.
2) Al comenzar la animación, se obtiene la hora del equipo y se guarda en una variable: start.
3) Cada vez que realizamos los cálculos para avanzar al siguiente fotograma, realizamos los siguientes pasos:
     a) Volvemos a tomar la hora del equipo y la guardamos en una segunda variable: now.
     b) Vemos el tiempo transcurrido desde el inicio de la animación hasta el momento actual (now-start) y lo comparamos con la duración total del efecto:
porciento=(now-start)/ms.
    c) Pasamos ese valor a una ecuación (variable curva) que nos devuelva la longitud del avance que debemos aplicar en la visualización de nuestro fotograma.

Veamos cómo sería eso traducido a código:
function transicion(curva,ms,callback){
    
this.ant=0.01;
    
this.done_=false;
    var 
_this=this;
    
this.start=new Date().getTime();
    
this.init=function(){
        
setTimeout(function(){
                if(!
_this.next()){
                    
callback(1);
                    
_this.done_=true;
                    
window.globalIntervalo=0;
                    return;
                }
                
callback(_this.next());
                
_this.init();
            },
13);
    }
    
this.next=function(){
        var 
now=new Date().getTime();
        if((
now-this.start)>ms)
            return 
false;
        return 
this.ant=curva((now-this.start+.001)/ms,this.ant);
    }
}



Antes de continuar, una aclaración sobre el uso de setTimeout en lugar de setInterval. Si bien es cierto que usar setInterval mejoraría la legibilidad del código y evitaría llamadas recursivas expresas al método init de nuestro objeto transicion, en todas las pruebas de rendimiento que realicé setTimeout arrojó mejores resultados que setInterval. Por eso yo me decanté por él, pero cada uno puede usar lo que más le guste.

Aclarado eso, podemos explicar un poco el código. No bien instanciemos el objeto transicion se definirá la propiedad start y en ella se almacenará la hora del equipo (new Date().getTime()). Luego, el método init iniciará la animación. Algún purista podrá argumentar que el seteo de la variable start debería estar definido no en la instanciación sino en el llamado al método init, el cual debería, por ejemplo, invocar a un método run que corra la animación... es cierto, pero como cada instancia del objeto transicion está pensada para usarse una vez por cada transición y luego destruirse, preferí hacerlo de esta manera y ahorrarme líneas de código.
Cada vez que el método init es invocado, se evalúa, mediante el método next, si el tiempo transcurrido es o no menor que la duración (variable ms) del efecto. Si es menor, next nos devuelve el valor porcentual que debemos aplicar a nuestra animación (next obtiene ese valor del retorno de una función (curva) a la que le pasa como argumento el porcentaje de tiempo de proceso transcurrido respecto de la duración del efecto).
Si en cambio el tiempo transcurrido supera la duración del efecto, next devuelve false y la animación termina.

Eso, a grandes rasgos. Volvamos a ver el método init y qué sucede cuando next devuelve un valor diferente de false. Cuando eso sucede, se ejecuta la función callback (que es la encargada de mostrar los cambios y que puede ser definida como más convenga), pasándole como argumento el valor devuelto por next.
Los valores que devuelve next van de 0 a 1 y representan el porcentaje a aplicar a la animación. Por supuesto, para poder usar dichos valores, la animación debe contar con un punto de inicio y un punto de finalización, de manera que podamos restarlos para obtener el 100% del recorrido y sobre él aplicar el valor devuelto por next para determinar el nuevo estado del objeto a animar.

Luego de ejecutar la función callback, el método init se llama a sí mismo y todo comienza de nuevo.

En alguna de las llamadas recursivas al método init, next devolverá false. Cuando eso ocurra, init invocará por última vez a la función callback, pasándole el porcentaje máximo que ésta espera (1), y finalizará su ejecución.

Veamos cómo aplicar todo eso que hemos dicho en nuestro primer ejemplo.


El código de ese ejemplo es el siguiente:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>test</title>
<style>
*{margin:0}
body{ background-color:#000; font-family:Verdana, Arial, Helvetica, sans-serif; font-size:9px}
#cuadro{width:50px; height:50px; background-color:orange; position:absolute; text-align:center; line-height:50px; color:white; margin-top:85px; cursor:pointer}
</style>
<script>
function 
transicion(curva,ms,callback){
    
this.ant=0.01;
    
this.done_=false;
    var 
_this=this;
    
this.start=new Date().getTime();
    
this.init=function(){
        
setTimeout(function(){
                if(!
_this.next()){
                    
callback(1);
                    
_this.done_=true;
                    
window.globalIntervalo=0;
                    return;
                }
                
callback(_this.next());
                
_this.init();
            },
13);
    }
    
this.next=function(){
        var 
now=new Date().getTime();
        if((
now-this.start)>ms)
            return 
false;
        return 
this.ant=curva((now-this.start+.001)/ms,this.ant);
    }
}
function 
mover(o){
    var 
inicio=0,fin=350;
    var 
t=new transicion(linear,1000,function(p){
        
o.style.left=inicio+((fin-inicio)*p)+'px';
    });
    
t.init();
    
t=null;
}
function 
linear(p,a){
    return 
p;
}
</
script>  
</head>
<body>
<div id="cuadro" onclick="mover(this)">click</div>
</body>
</html>



Como podemos observar, dentro de la función mover contamos con los valores inicio y fin, que representan, respectivamente, el punto de arranque y finalización de nuestra animación. Teniendo esos 2 puntos, podemos calcular el total del recorrido (fin-inicio) y determinar la nueva posición del objeto o aplicando a esa distancia total el porcentaje p que nos entregará el método init cada vez que invoque nuestro callback.

En el ejemplo usamos la función auxiliar linear. Esa función corresponde al primer argumento (curva) del constructor del objeto transicion, y determina la cadencia de la animación.
En el caso analizado, la función sólo retorna el porcentaje correspondiente al tiempo transcurrido respecto de la duración del efecto, por lo que el objeto evoluciona a velocidad constante.
Pero usando funciones que no entreguen el mismo valor que reciben sino otros que resulten de un proceso matemático, podremos obtener animaciones de diferentes tipos: efectos de aceleración, de desaceleración, de elasticidad, etc.

Nosotros aquí mostraremos sólo los efectos más usados y calculados a la antigua, pero recomendamos investigar las ecuaciones de Robert Penner para lograr efectos más avanzados.

Efecto de Desaceleración

Para lograrlo, nuestra función curva podría tener el siguiente aspecto:

function desacelerado(p,ant){
    var 
maxValue=1minValue=.001totalP=1k=.25;
    var 
delta maxValue minValue
    var 
stepp minValue+(Math.pow(((totalP) * p), k) * delta); 
    return 
stepp
}



Aplicándola al ejemplo anterior, obtendríamos el siguiente resultado:


El código que utilizamos para el ejemplo anterior es este:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>test</title>
<style>
*{margin:0}
body{ background-color:#000; font-family:Verdana, Arial, Helvetica, sans-serif; font-size:9px}
#cuadro{width:50px; height:50px; background-color:orange; position:absolute; text-align:center; line-height:50px; color:white; margin-top:85px; cursor:pointer}
</style>
<script>
function 
transicion(curva,ms,callback){
    
this.ant=0.01;
    
this.done_=false;
    var 
_this=this;
    
this.start=new Date().getTime();
    
this.init=function(){
        
setTimeout(function(){
                if(!
_this.next()){
                    
callback(1);
                    
_this.done_=true;
                    
window.globalIntervalo=0;
                    return;
                }
                
callback(_this.next());
                
_this.init();
            },
13);
    }
    
this.next=function(){
        var 
now=new Date().getTime();
        if((
now-this.start)>ms)
            return 
false;
        return 
this.ant=curva((now-this.start+.001)/ms,this.ant);
    }
}
function 
mover(o){
    var 
inicio=0,fin=350;
    var 
t=new transicion(desacelerado,1000,function(p){
        
o.style.left=inicio+((fin-inicio)*p)+'px';
    });
    
t.init();
    
t=null;
}
function 
desacelerado(p,ant){
    var 
maxValue=1minValue=.001totalP=1k=.25;
    var 
delta maxValue minValue
    var 
stepp minValue+(Math.pow(((totalP) * p), k) * delta); 
    return 
stepp
}
</
script>  
</head>
<body>
<div id="cuadro" onclick="mover(this)">click</div>
</body>
</html>



Se aprecia que el código es casi idéntico. Sólo cambia la curva, que se asigna mediante la instrucción:
var t=new transicion(desacelerado,1000,function(p){...


Efecto de Aceleración

La curva puede tener el siguiente aspecto:
function acelerado(p,ant){
    var 
maxValue=1minValue=.001totalP=1k=7;
    var 
delta maxValue minValue
    var 
stepp minValue+(Math.pow(((totalP) * p), k) * delta); 
    return 
stepp
}



Aplicándola, obtenemos:


Movimiento Elástico Suave

Curva posible:
function elasticoSuave(p,ant){
    if(
p<=0.6){
        return 
Math.pow(p*5/3,2);
    }else{
        return 
Math.pow((p-0.8)*5,2)*0.4+0.6;
    }
}



Resultado:


Movimiento Elástico Fuerte

Curva posible:
function elasticoFuerte(p,ant){
    if(
p<=0.6){
        return 
Math.pow(p*5/3,2);}
    else{
        return 
Math.pow((p-0.8)*5,2)*0.6+0.6;
    }
}



Resultado:


Movimiento Senoidal

Curva posible:
function senoidal(p,ant){
    return (
Math.cos(Math.PI)) / 2;
}



Resultado:


Creando un Marco de Trabajo para Transiciones

Lo que vimos hasta ahora es poderoso, pero difícil de usar. Veamos cómo hacerlo más usable, pero paso a paso.
Primero vamos crear una función a la que le pasemos como argumentos el objeto a animar,la propiedad css a utilizar, el estado inicial de dicha propiedad, el estado final, el tiempo en milisegundos y la curva que definirá la modalidad del efecto.
Dicha función podría tener esta estructura:
function fx(obj,inicio,fin,propCss,u,curva,ms){
    var 
t=new transicion(curva,ms,function(p){
        if(
fin<inicio){
            var 
delta=inicio-fin;
            
obj.style[propCss]=(inicio-(p*delta))+u;
        }
        else{
            var 
delta=fin-inicio;
            
obj.style[propCss]=(inicio+(p*delta))+u;
        }
    });
    
t.init();
    
t=null;
}


Es bastante más usable, aunque tiene algunos problemas que ya iremos comentando y solucionando. Entretanto, veamos un ejemplo de aplicación usando este código:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>test</title>
<style>
*{margin:0}
body{ background-color:#000; font-family:Verdana, Arial, Helvetica, sans-serif; font-size:9px}
#cuadro{width:50px; height:50px; background-color:orange; position:absolute; text-align:center; line-height:50px; color:white; margin-top:85px; cursor:pointer}
</style>
<script>
function 
transicion(curva,ms,callback){
    
this.ant=0.01;
    
this.done_=false;
    var 
_this=this;
    
this.start=new Date().getTime();
    
this.init=function(){
        
setTimeout(function(){
                if(!
_this.next()){
                    
callback(1);
                    
_this.done_=true;
                    
window.globalIntervalo=0;
                    return;
                }
                
callback(_this.next());
                
_this.init();
            },
13);
    }
    
this.next=function(){
        var 
now=new Date().getTime();
        if((
now-this.start)>ms)
            return 
false;
        return 
this.ant=curva((now-this.start+.001)/ms,this.ant);
    }
}

function 
fx(obj,inicio,fin,propCss,u,curva,ms){
    var 
t=new transicion(curva,ms,function(p){
        if(
fin<inicio){
            var 
delta=inicio-fin;
            
obj.style[propCss]=(inicio-(p*delta))+u;
        }
        else{
            var 
delta=fin-inicio;
            
obj.style[propCss]=(inicio+(p*delta))+u;
        }
    });
    
t.init();
    
t=null;
}
function 
senoidal(p,ant){
    return (
Math.cos(Math.PI)) / 2;
}
</
script>  
</head>
<body>
<div id="cuadro" onclick="fx(this,50,350,'width','px',senoidal,1000)">click</div>
</body>
</html>


Resultado:


El primer problema que podemos notar es que si hacemos un segundo click sobre el objeto antes de que termine la animación, se superponen 2 transiciones (la que estaba corriendo más la nueva generada por este segundo click), creando un efecto desagradable.

En realidad, ya habíamos pensado en ese problema antes, y es por eso que habíamos agregado, dentro del objeto transicion, la variable global window.globalIntervalo, la cual aparece puesta a cero cuando la transición termina. Y es que planificamos usarla desde afuera, para que si se intentaba generar una transición nueva mientras otra estaba ejecutándose, pudiéramos hacer algo al respecto. Y lo que podemos hacer es poner esa nueva transición en espera hasta que la anterior termine, o simplemente ignorarla.
Para manejar este nuevo comportamiento agregaremos a nuestra función fx un nuevo argumento llamado cola, el cual, puesto a true encolará la nueva animación y la ejecutará cuando la primera termine, y puesto a false la descartará.
En código, sería algo así:

function fx(obj,inicio,fin,propCss,u,curva,ms,cola){
    if(!
window.globalIntervalo)
        
window.globalIntervalo=1;
    else {
        if(
cola)
            return 
setTimeout(function(){fx(obj,inicio,fin,propCss,u,curva,ms,cola);},30);
        else
            return;
    }    
    var 
t=new transicion(curva,ms,function(p){
        if(
fin<inicio){
            var 
delta=inicio-fin;
            
obj.style[propCss]=(inicio-(p*delta))+u;
        }
        else{
            var 
delta=fin-inicio;
            
obj.style[propCss]=(inicio+(p*delta))+u;
        }
    });
    
t.init();
    
t=null;
}



Veamos cómo resulta esta nueva funcionalidad en ejemplos (deberás hacer click varias veces para comprobar el funcionamiento):
a)Argumento cola puesto a true (memorizar solicitudes y ejecutarlas cuando haya disponibilidad)



b)Argumento cola puesto a false (ignorar nuevas solicitudes hasta que la transición termine)



Bonito. ¿Pero qué pasa si quiero modificar en simultáneo 2 propiedades css?
Veamos el resultado de este código:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>test</title>
<style>
*{margin:0}
body{ background-color:#000; font-family:Verdana, Arial, Helvetica, sans-serif; font-size:9px}
#cuadro{width:50px; height:50px; background-color:orange; position:absolute; text-align:center; line-height:50px; color:white; top:30px;left:30px;cursor:pointer}
</style>
<script>
function 
transicion(curva,ms,callback){
    
this.ant=0.01;
    
this.done_=false;
    var 
_this=this;
    
this.start=new Date().getTime();
    
this.init=function(){
        
setTimeout(function(){
                if(!
_this.next()){
                    
callback(1);
                    
_this.done_=true;
                    
window.globalIntervalo=0;
                    return;
                }
                
callback(_this.next());
                
_this.init();
            },
13);
    }
    
this.next=function(){
        var 
now=new Date().getTime();
        if((
now-this.start)>ms)
            return 
false;
        return 
this.ant=curva((now-this.start+.001)/ms,this.ant);
    }
}

function 
fx(obj,inicio,fin,propCss,u,curva,ms,cola){
    if(!
window.globalIntervalo)
        
window.globalIntervalo=1;
    else {
        if(
cola)
            return 
setTimeout(function(){fx(obj,inicio,fin,propCss,u,curva,ms,cola);},30);
        else
            return;
    }    
    var 
t=new transicion(curva,ms,function(p){
        if(
fin<inicio){
            var 
delta=inicio-fin;
            
obj.style[propCss]=(inicio-(p*delta))+u;
        }
        else{
            var 
delta=fin-inicio;
            
obj.style[propCss]=(inicio+(p*delta))+u;
        }
    });
    
t.init();
    
t=null;
}
function 
senoidal(p,ant){
    return (
Math.cos(Math.PI)) / 2;
}
</
script>  
</head>
<body>
<div id="cuadro" onclick="fx(this,50,350,'width','px',senoidal,1000,true);fx(this,50,350,'height','px',senoidal,1000,true)">click</div>
</body>
</html>


Resultado:


Como vemos, tal como está planteada, nuestra función fx no nos sirve para efectos simultáneos de este tipo: ambos efectos se producen, pero de manera separada, y esto gracias a que el argumento cola está a true.

Otro problema que le veo es que no podríamos usarla para lograr efectos de transparencia por el camino al que nos obliga Explorer al usar filter. Veamos cómo podemos modificarla para que conserve los avances logrados y además incorpore estas nuevas funcionalidades:
function fx(obj,efectos,ms,cola,curva){
    if(!
window.globalIntervalo)
        
window.globalIntervalo=1;
    else {
        if(
cola)
            return 
setTimeout(function(){fx(obj,efectos,ms,cola,curva)},30);
        else
            return;
    }    
    var 
t=new transicion(
    
curva,ms,function(p){
        for (var 
i=0;efectos[i];i++){
            if(
efectos[i].fin<efectos[i].inicio){
                var 
delta=efectos[i].inicio-efectos[i].fin;
                
obj.style[efectos[i].propCSS]=(efectos[i].inicio-(p*delta))+efectos[i].u;
                if(
efectos[i].propCSS=='opacity'){
                    
obj.style.zoom=1;
                    
obj.style.MozOpacity = (efectos[i].inicio-(p*delta));
                    
obj.style.KhtmlOpacity = (efectos[i].inicio-(p*delta));
                    
obj.style.filter='alpha(opacity='+100*(efectos[i].inicio-(p*delta))+')';
                }
            }
            else{
                var 
delta=efectos[i].fin-efectos[i].inicio;
                
obj.style[efectos[i].propCSS]=(efectos[i].inicio+(p*delta))+efectos[i].u;
                if(
efectos[i].propCSS=='opacity'){
                    
obj.style.zoom=1;
                    
obj.style.MozOpacity = (efectos[i].inicio+(p*delta));
                    
obj.style.KhtmlOpacity = (efectos[i].inicio+(p*delta));
                    
obj.style.filter='alpha(opacity='+100*(efectos[i].inicio+(p*delta))+')';
                }
            }
        }
        
    });
    
t.init();
    
t=null;
}



Se ve bastante diferente, pero no lo es tanto. En realidad, agrupamos los argumentos inicio, fin, propiedadCss y u (unidad de medida de la propiedad css) en un vector de objetos (llamado efectos), de manera tal que podamos recorrerlo con un bucle.
Además agregamos controles para que, en caso de que la propiedad css sea opacity, funcione igual en todos los navegadores modernos (y no tan modernos).
Ahora podemos aplicar varios efectos de manera simultánea invocándolos de esta forma:
function $(x){return document.getElementById(x);}
onload=function(){
$(
'cuadro').onclick=function(){
    
fx($('cuadro'),[
    {
'inicio':0,'fin':350,'u':'px','propCSS':'width'},
    {
'inicio':0,'fin':350,'u':'px','propCSS':'height'},
    {
'inicio':0,'fin':1,'u':'','propCSS':'opacity'}
    ],
1000,true,desacelerado);
}
}


Aplicando ese código obtendríamos el siguiente efecto:


Podríamos seguir agregando funcionalidad a este micro framework de animaciones javascript, pero tal como está ya da cobertura a los efectos más usados, de manera que por ahora lo dejaremos así. Es posible que en algún otro artículo muestre ejemplos más interesantes de las cosas que podemos hacer con él (ahora el objetivo no era la pirotecnia sino ser lo más didáctico posible).

Código Final de nuestro Marco de Trabajo para Transiciones

<script>
function 
transicion(curva,ms,callback){
    
this.ant=0.01;
    
this.done_=false;
    var 
_this=this;
    
this.start=new Date().getTime();
    
this.init=function(){
        
setTimeout(function(){
                if(!
_this.next()){
                    
callback(1);
                    
_this.done_=true;
                    
window.globalIntervalo=0;
                    return;
                }
                
callback(_this.next());
                
_this.init();
            },
13);
    }
    
this.next=function(){
        var 
now=new Date().getTime();
        if((
now-this.start)>ms)
            return 
false;
        return 
this.ant=curva((now-this.start+.001)/ms,this.ant);
    }
}

function 
linear(p,ant){
    var 
maxValue=1minValue=.001totalP=1k=1;
    var 
delta maxValue minValue
    var 
stepp minValue+(Math.pow(((totalP) * p), k) * delta); 
    return 
stepp
}
function 
senoidal(p,ant){
    return (
Math.cos(Math.PI)) / 2;
}

function 
desacelerado(p,ant){
    var 
maxValue=1minValue=.001totalP=1k=.25;
    var 
delta maxValue minValue
    var 
stepp minValue+(Math.pow(((totalP) * p), k) * delta); 
    return 
stepp
}

function 
acelerado(p,ant){
    var 
maxValue=1minValue=.001totalP=1k=7;
    var 
delta maxValue minValue
    var 
stepp minValue+(Math.pow(((totalP) * p), k) * delta); 
    return 
stepp
}

function 
elasticoFuerte(p,ant){
    if(
p<=0.6){
        return 
Math.pow(p*5/3,2);}
    else{
        return 
Math.pow((p-0.8)*5,2)*0.6+0.6;
    }
}

function 
elasticoSuave(p,ant){
    if(
p<=0.6){
        return 
Math.pow(p*5/3,2);
    }else{
        return 
Math.pow((p-0.8)*5,2)*0.4+0.6;
    }
}


function 
fx(obj,efectos,ms,cola,curva){
    if(!
window.globalIntervalo)
        
window.globalIntervalo=1;
    else {
        if(
cola)
            return 
setTimeout(function(){fx(obj,efectos,ms,cola,curva)},30);
        else
            return;
    }    
    var 
t=new transicion(
    
curva,ms,function(p){
        for (var 
i=0;efectos[i];i++){
            if(
efectos[i].fin<efectos[i].inicio){
                var 
delta=efectos[i].inicio-efectos[i].fin;
                
obj.style[efectos[i].propCSS]=(efectos[i].inicio-(p*delta))+efectos[i].u;
                if(
efectos[i].propCSS=='opacity'){
                    
obj.style.zoom=1;
                    
obj.style.MozOpacity = (efectos[i].inicio-(p*delta));
                    
obj.style.KhtmlOpacity = (efectos[i].inicio-(p*delta));
                    
obj.style.filter='alpha(opacity='+100*(efectos[i].inicio-(p*delta))+')';
                }
            }
            else{
                var 
delta=efectos[i].fin-efectos[i].inicio;
                
obj.style[efectos[i].propCSS]=(efectos[i].inicio+(p*delta))+efectos[i].u;
                if(
efectos[i].propCSS=='opacity'){
                    
obj.style.zoom=1;
                    
obj.style.MozOpacity = (efectos[i].inicio+(p*delta));
                    
obj.style.KhtmlOpacity = (efectos[i].inicio+(p*delta));
                    
obj.style.filter='alpha(opacity='+100*(efectos[i].inicio+(p*delta))+')';
                }
            }
        }
        
    });
    
t.init();
    
t=null;
}
</
script>



Anotaciones Finales

Como premio o castigo para quien haya llegado al final de este tedioso artículo, dejo una serie de tips relacionados con animaciones, cuidadosamente cosechados en huertos propios y ajenos:


-La fluidez de una animación es inversamente proporcional a los recursos que debe usar el navegador para mostrarla.
-Los recursos que usa el navegador durante una animación son directamente proporcionales a la cantidad de propiedades css involucradas.
-Al navegador le cuesta más mover un elemento div con una imagen de background que mover un elemento img.
-Aunque a muchos les duela, innerHTML es más rápido que DOM en la mayoría de los casos.
-Es mala idea usar la propiedad length en el condicional de un bucle, ya que la misma es reinterpretada en cada una de las iteraciones del bucle: mejor no usarla o hacerlo fuera del condicional del bucle.
-Acostumbrate a poner a null las variables que no uses.
-Si en determinado momento tenés que aplicar una animación idéntica a varios elementos, en lugar de hacer una animación para cada uno de ellos es mejor agruparlos en un contenedor y realizar una única animación.
-Si tu animación involucra imágenes, asegurate de que la propiedad complete de las mismas sea igual a true antes de que la transición comience.



Home - Quiénes Somos - Portfolio - Espacio Diseño - Espacio Programación - Capacitación - Contacto - RSS