Animaciones en Windows 8 con HTML5 y JavaScript


movement
En las aplicaciones HTML/JavaScript tenemos dos formas de crear animaciones:

  • De forma declarativa, mediante CSS3 @keyframes o utilizando la etiqueta <animate> de SVG
  • Mediante una de las funciones de temporizador que incorpora el lenguaje JavaScript

Las primeras son muy útiles para los diseñadores en las animaciones de cambio de estado de elementos del UI, pero al ser declarativas nos limitan a acciones muy concretas. Si lo que necesitamos es algo dinámico o queremos animar pintando sobre un elemento canvas, como por ejemplo en el caso de los juegos, tendremos que utilizar JavaScript.

Animaciones con temporizador

Hasta hace poco, las animaciones utilizando JavaScript se realizaban utilizando los temporizadores que incorpora el lenguaje, por ejemplo la función setInterval nos permite ejecutar una función de forma cíclica:

function animationLoop(){
  //cálculos
  update();
  //código de pintado
  draw();
}
setInterval(animationLoop,50);

Otra opción era utilizar setTimeout para ejecutar la animación; es menos eficiente que la anterior, pues tenemos que llamar cada vez a la función para continuar, pero obtenemos un mejor control:

function animationLoop(){
  setTimeout(animationLoop, 50);
  //cálculos
  update();
  //código de pintado
  draw();
}
animationLoop();

Nota: estoy escribiendo el bucle de animación de forma muy similar a cómo se utiliza en otros sistemas como XNA, en los que se separan los cálculos de movimiento de la operación de pintado en pantalla.

FPS

La clave para que el movimiento de una animación sea suave es engañar a la vista, actualizando la imagen lo más rápido posible. Todo esto es debido a un fenómeno que ocurre dentro de nuestros ojos llamado persistencia de la visión; gracias a él nos bastará con actualizar las imágenes más rápido que lo que nuestro ojo es capaz de distinguir. En cine lo habitual es utilizar 24 imágenes por segundo (Frames Per Second = Fotogramas Por Segundo), en televisión 25 o 30 dependiendo del sistema y en videojuegos se considera aceptable a partir de 30 pero algunos superan los 100FPS.

Para conseguir el efecto que deseamos con el código anterior debemos dividir 1000 entre las imágenes por segundo que deseamos y así obtendremos el número de milisegundos que deberíamos esperar entre refrescos:

var FPS=1000/60; // =16,6, es decir aproximadamente cada 17ms
function animationLoop(){
  setTimeout(animationLoop, FPS);
  //cálculos
  update();
  //código de pintado
  draw();
}
animationLoop();

FPS y refresco real de pantalla

Hasta aquí hemos establecido nosotros el refresco que deseamos en pantalla, pero todos los códigos anteriores presentan una serie de problemas:

  1. Refresco:
    • La combinación de pantalla y tarjeta gráfica del equipo donde se ejecute nuestra aplicación nos proporcionarán una frecuencia de refresco que puede no ser la misma que la que hemos programado. Si nuestra frecuencia es mayor que la del equipo estamos realizando trabajo de más inútilmente pues no se verá el resultado. De nada nos sirve conseguir 120FPS si la pantalla sólo es capaz de mostrar 50FPS
    • Puede que el tiempo de cálculo dentro de la función update sea mayor que el tiempo de refresco, con lo que estaríamos en realidad por debajo de los FPS que habíamos definido.
    • Los temporizadores de los navegadores no son precisos al milisegundo y cada uno de los navegadores tiene una resolución diferente que varía entre los 4ms y los 15ms aproximadamente. Esto significa que aunque coloquemos un intervalo que se ajuste al refresco de la pantalla sólo obtendremos un valor aproximado
  2. Uso del procesador:
    • Las funciones setTimeout y setInterval siempre ocurren aunque la ventana no esté visible, en Windows 8 no representa un problema, pues las aplicaciones que no están en primer plano no se ejecutan, pero en un navegador seguirá consumiendo procesador aunque estemos mirando otra pestaña.
    • Si ejecutamos más ciclos de pintado de los que el equipo puede mostrar estamos malgastando tiempo de procesador y provocando un gasto inútil de batería en el caso de equipos portátiles.

requestAnimationFrame al rescate

Para resolver estos problemas el W3C creó la función requestAnimationFrame basada en una primera versión que se creó en el navegador Firefox. Esta función garantiza que se ejecutará durante el siguiente refresco de pantalla y sólo si la ventana está visible. Se utiliza de forma muy parecida al ejemplo anterior usando la función setTimeOut:

function animationLoop(time){
  requestAnimationFrame(animationLoop);
  //cálculos
  update(time);
  //código de pintado
  draw(time);
}
animationLoop(0);

Podemos ver un ejemplo de comparación de la eficiencia de ambos métodos en la página de demostración de IE10.

Control del tiempo

En los viejos tiempos, para calcular el movimiento de una animación se utilizaba un incremento simple, es decir, cada fotograma se aumentaba una cantidad más o menos fija a la posición del elemento a animar, pero hace muchos años que eso ya no sirve, pues hoy en día cada dispositivo va a una velocidad diferente, e incluso algunos ajustan su velocidad de reloj dependiendo de la cantidad de batería que quede. Así pues, ese método no nos servirá para mucho. Incluso con los métodos anteriores en los que nosotros fijábamos el número de fotogramas por segundo, nada nos garantiza que nuestro temporizador se ejecute siempre a la misma velocidad.
Si nos fijamos en la firma de la función requestAnimationFrame veremos que recibe como parámetro un tiempo. Utilizando este tiempo podremos calcular cuánto tiempo pasó desde la última vez que se ejecuto nuestra función y con ese valor delta podremos calcular la siguiente posición del elemento animado.

Ejemplo de animación

Vamos a crear una animación sencilla de un punto realizando un movimiento circular.
En una aplicación JavaScript de Windows Store crearemos un elemento canvas con id animationCanvas.

<body>
    <canvas id="animationCanvas">

    </canvas>
</body>

En el script default.js añadiremos una llamada a la promise processAll de la siguiente forma:

args.setPromise(WinJS.UI.processAll().then(start));

Fuera de la declaración del evento onActivated añadimos el código que ejecutará la animación:

var context2d;
function start() {
    resizeCanvas();
    context2d = animationCanvas.getContext("2d");
    loop(0);
}

function resizeCanvas() {
    animationCanvas.height = window.innerHeight;
    animationCanvas.width = window.innerWidth;
}

function loop(time) {
    requestAnimationFrame(loop);
    update(time);
    draw(time);
}

En la función update vamos a calcular una animación cíclica y circular:

var center = [200, 200];
var radius=50;
var pix2=Math.PI*2;
var speed=pix2/1.5;

var oldTime=0;
var rotation=0;
var position=[center[0]+radius,center[1]];

function update(time) {
    var delta = time - oldTime;
    oldTime = time;
    rotation+= speed * delta/1000;
    if(rotation>pix2)
        rotation-=pix2;
    position=[center[0]+radius*Math.sin(rotation),
        center[1]+radius*Math.cos(rotation)];
}

Y para acabar, en el método draw dibujamos el círculo en la posición calculada:

function draw(time) {
    context2d.clearRect(0, 0, animationCanvas.width, animationCanvas.height);
    context2d.fillStyle = '#fff';
    context2d.beginPath();
    context2d.arc(position[0],position[1], 20, 0, pix2);
    context2d.fill();
}

Conclusión

Con la nueva API requestAnimationFrame podemos crear animaciones con la frecuencia más adecuada para el dispositivo que la esté mostrando de forma muy sencilla. Esto nos permite conseguir animaciones suaves y con un rendimiento energético óptimo, muy importante en dispositivos móviles.

¿Qué herramientas necesito?

Descarga las herramientas para desarrolladores en msdn.

Anuncios

  1. Pingback: Motor de animaciones para aplicaciones HTML (Web + Windows Store) « Mouseless Me
  2. jorge

    Matemáticamente ablando, la expresión “rotation+= speed * delta/1000;” baldra siempre 0 (cero) por sus valores iniciales

  3. jmservera

    Hola Jorge,
    Gracias por tu comentario. Si te fijas, la función update recibe un parámetro time y maneja dos valores globales: oldtime y rotation. En el momento 0 los tres valores (time, oldtime, rotation) valen 0, pero al cabo de 16 o 17 ms aproximadamente (para conseguir 60fps) time valdrá 16 y oldtime valdrá 0, así que:
    delta= 16 – 0;
    oldtime=16;
    rotation+=6/2 Pi * 16/1000;
    Entonces en el momento 16ms tendremos los siguientes valores:
    delta=16, oldtime=16 y rotation=0,15 (aprox)
    Y seguirá creciendo. Es importante utilizar un valor delta para realizar los cálculos, pues nada nos garantiza que el sistema no se salte frames, así que puede que la siguiente iteración sea más corta o más larga de los 16,66 ms que necesitamos para garantizar los 60fps.
    Puedes ver cómo utilizo esta técnica online en http://jsfiddle.net/jmservera/upxFY/embedded/result/ y en el artículo https://jmservera.com/2012/11/09/motor-de-animaciones-para-aplicaciones-html-web-windows-store/
    Un saludo,
    Juanma

  4. ivan

    hola yo no utilizo canvas pero supongo que deve aber algo que me ayude bueno aqui esta mi pregunta, e querido crear algunos videojuegos pero siempre los dejo ni casi a la mitad porque cuando muebo el scroll y la imagen al mismo tiempo la imagen tiembla muchisimo escribi este codigo para probar que podia hacer, espero tu ayuda porfa, se que c puede lograr sin canvas por que tengo un juego que esta echo sin el y funciona de marabilla a qui dejo mi codigo de prueba
    ********************************************************************************************************************************************************

    l

    var hhh= false;
    var Gl_iebuffer;
    var x2=0;

    var mo = function(x) {
    this.x = x;

    t=this;
    t.raw=document.createElement(‘IMG’);
    document.getElementsByTagName(“body”).item(0).appendChild(this.raw);/////////////////////////////////////ESTA
    t.raw.style.position=”absolute”;

    t.moveTo = iner;
    t.raw.src=”0.png”
    t.width = this.raw.offsetWidth;
    t.height = this.raw.offsetHeight;
    t.raw.style.overflow=”auto”;
    t.ob=this.raw.style;
    this.width=this.raw.clientWidth;
    this.height=this.raw.clientHeight;

    return t;

    }

    function iner(x,y) {
    this.x=parseInt(x);this.y=parseInt(y);
    this.ob.left = this.y+’px’;
    this.ob.left = this.x+’px’;

    }
    op = new mo(0);

    var mo2 = function() {

    if(hhh == true) {

    var delta = (new Date().getTime()) – this.tiempoTranscurrido;
    this.tiempoTranscurrido=new Date().getTime();
    this.velo = 500.500;

    op.x += ((delta+.001)/1000*this.velo);

    op.moveTo(parseInt(op.x),0);
    scrollTo(op.x-300,0);

    }

    if(hhh == false) {
    this.tiempoTranscurrido=new Date().getTime();
    hhh=true;
    }
    fps=60;
    Gl_fps=1000/fps;
    var loop = setInterval(“mo2()”,Gl_fps);
    }

    mo2()

    ***********************************************************************************************************************************************************

    te doy las gracias de antemano por la ayuda gracias.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s