Animaciones usando Tween y Threej

Que es y para que sirve requestAnimationFrame?

Tradicionalmente cuando se creaban animaciones javascript usabamos el metodo setTimeout() y el setInterval(), esto estaba bien, pero ahora con requestAnimationFrame() las cosas son aún mejores. la sintaxis de requestAnimationFrame() es la siguiente:

requestAnimationFrame(function);

Donde function es la función que controlarar el progreso de la animación en cada frame, permitiendonos un control más fino entre frame y frame. El metodo es llamado cada vez que la ventana tiene que actualizar un nuevo frame. Cuando usamos el metodo requestAnimationFrame() para crear una animación nos aseguramos que la computadora de nuestro cliente este preparada para renderizar un nuevo frame, esto nos permite programar animaciones más fluidas que utlizan mejor los recursos del cliente, con el methodo anterior, desaprovechabamos muchos recursos entre setInterval() y setInterval().

var adiv = document.getElementById('mydiv')
var leftpos = 0
function movediv(timestamp){
    leftpos += 5
    adiv.style.left = leftpos + 'px'
    requestAnimationFrame(movediv) // Llamar de nuevo requesAnimationFrame para animar el siguiente frame
}
requestAnimationFrame(movediv) // Llamar requestAnimationFrame y pasarle función de animación

Podemos cancelar un loop requestAnimationFrame haciendo uso de cancelAnimationFrame(), para ello tenemos que hacer global la llamada interna de requestAnimationFram, en el ejemplo he almacenado el requestAnimationFrame en reqanimationreference para que pudiera detenerla usando cancelAnimationFrame()

var reqanimationreference;

function logtimestamp(timestamp){
    console.log(timestamp);
    reqanimationreference = requestAnimationFrame(logtimestamp);
}

requestAnimationFrame(logtimestamp);

setTimeout(function(){ // cancelamos el funcionamiento de requestAnimationFrame despues de 2 segundos
    cancelAnimationFrame(reqanimationreference);
}, 2000)

Tween

Como mencionambamos anteriormente en la actualidad utilizamos requestAnimationFrame para renderizar un frame con mejor rendimiento, en javascript actual un frame se ve así:

function loop() {
    // Un simple frame
    requestAnimationFrame(loop);
}

Un tween es simplemente un array que en combinación con una función controla el intermedio entre diferentes frames, de forma tal que la animación se efectue en el tiempo y de la forma deseada, el nombre tween debiene justamente de betweening que significa en ingles entremedio.

var tween = {
  startTime: 0,
  startX: 0,
  startY: 0,
  startZ: 0,
  endX: 0,
  endY: 0,
  endZ: 0,
  startRotationX: 0,
  startRotationY: 0,
  startRotationZ: 0,
  endRotationX: 0,
  endRotationY: 0,
  endRotationZ: 0,
  duration: 0
}

A esta estructura de datos la podemos modificar siempre que necesitemos activar una animación, en nuestro caso particular lo hacemos mediante un evento click.

window.addEventListener('click', activarAnim);
var left;
function activarAnim(){
    // Invertimos la animación
    if(left){
        left = false;
        tween.startTime = Date.now();
        tween.duration = 500;
        tween.startRotationY = 2.2;
        tween.endRotationY = 1.8;
    }
    // Animación normal
    else {
        left = true;
        tween.startTime = Date.now();
        tween.duration = 500;
        tween.startRotationY = 1.8;
        tween.endRotationY = 2.2;
    }
}

La animación la calculamos haciendo uso de una animación lineal, que es la más sencilla de lograr:

// Esta función nos permite calcular el valor de la transformacion
function linealAnimation(t,duration,startRotationY,endRotationY){
    // Calculamos el porcentaje del tiempo transcurrido
    var pct = t/duration;
    // Calculamos el rango de la animación y su función respecto
    // al porcentaje
    var tmp = ((startRotationY-endRotationY)*-1)*pct;
    // Sumamos el valor obtenido de transformación al valor inicial
    tmp += startRotationY;
    return tmp;
}

Luego en el loop del render vamos actualizando la animación

/*
 * Proceso de renderizado activado
 */

var render = function(){
    renderer.render(scene, camera);
    // Obtenemos el tiempo actual
    var t = Date.now() - tween.startTime;
    // Realizamos la actualización del tween
    if(t <= tween.duration){
        var transform = linealAnimation(t,tween.duration,tween.startRotationY,tween.endRotationY);
        mesh.rotation.y = Math.PI * transform;
    }

    requestAnimationFrame(render);
};
render();

Si has seguido los pasos descritos aquí tendrías algo similar a esto

window.addEventListener('click', activarAnim);

var scene = new THREE.Scene();

var camera = new THREE.PerspectiveCamera(
  75, 
  window.innerWidth/window.innerHeight, 
  0.1, 
  1000
);

camera.position.z = 100;

var renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
window.addEventListener( 'resize', function () {
                    renderer.setSize( window.innerWidth, window.innerHeight );
} );
document.body.appendChild( renderer.domElement );

var mesh = null;
var loader = new THREE.JSONLoader();
loader.load(
    'suzanne-monkey.json', 
    function(geometry, materials) {
        var material = materials[ 0 ];
        mesh = new THREE.Mesh(geometry, material);
        scene.add(mesh);
        mesh.scale.x = mesh.scale.y = mesh.scale.z = 11;
        console.log('Mono cargado correctamente');
    },
    function(xhr){
        console.log((xhr.loaded / xhr.total * 100) + '% loaded');
    },
    function (xhr) {
        console.log( 'un error a sucedido');
    }
);

var light = new THREE.PointLight(0xFFFF00);
light.position.set(10, 0, 25);
scene.add(light);

// Ejemplo de un tween
var tween = {
  startTime: 0,
  startX: 0,
  startY: 0,
  startZ: 0,
  endX: 0,
  endY: 0,
  endZ: 0,
  startRotationX: 0,
  startRotationY: 0,
  startRotationZ: 0,
  endRotationX: 0,
  endRotationY: 0,
  endRotationZ: 0,
  duration: 0
}

// Con esta función activamos la animación
var left;
function activarAnim(){
    if(left){
        left = false;
        tween.startTime = Date.now();
        tween.duration = 500;
        tween.startRotationY = 2.2;
        tween.endRotationY = 1.8;
    }
    else {
        left = true;
        tween.startTime = Date.now();
        tween.duration = 500;
        tween.startRotationY = 1.8;
        tween.endRotationY = 2.2;
    }
}

// Esta función nos permite calcular el valor de la transformacion
function linealAnimation(t,duration,startRotationY,endRotationY){
    var pct = t/duration;
    var tmp = ((startRotationY-endRotationY)*-1)*pct;
    tmp += startRotationY;
    return tmp;
}

/*
 * Proceso de renderizado activado
 */

var render = function(){
    renderer.render(scene, camera);
    // Obtenemos el tiempo actual
    var t = Date.now() - tween.startTime;
    // Realizamos la actualización del tween
    if(t <= tween.duration){
        var transform = linealAnimation(t,tween.duration,tween.startRotationY,tween.endRotationY);
        mesh.rotation.y = Math.PI * transform;
    }

    requestAnimationFrame(render);
};
render();