Etiquetado: HTML5

Las nuevas características de búsqueda en Windows 8.1

Windows 8.1 Preview presenta grandes novedades con respecto a la versión actual. Una que me ha chocado mucho es el nuevo sistema de búsqueda. Tiene cosas muy buenas y otras no tanto, pero tengamos en cuenta que todavía es una versión preliminar y puede cambiar mucho hasta el lanzamiento.

found dog

Desde el punto de vista del desarrollador, en la versión anterior era muy fácil y directo integrarse con el contrato de búsqueda; permitía a los usuarios buscar dentro de nuestra aplicación aunque ésta estuviera cerrada, haciendo que nuestra aplicación estuviera siempre presente.

Cookbook search old

A pesar de los esfuerzos de Microsoft por explicar las bondades del sistema de búsqueda, en el mundo real™ los usuarios no acababan de entender el sistema o simplemente no encontraban el botón de búsqueda dentro de la aplicación y pensaban que la aplicación no podía buscar. En las aplicaciones no había ninguna evidencia de que pudieran buscar y al final poníamos un botón dentro de la aplicación que abría el charm de búsqueda. Además, cuando empiezas a tener muchas aplicaciones que implementan el contrato de búsqueda la lista de posibilidades se vuelve demasiado larga para ser manejable.

Por este motivo y porque la funcionalidad del búsqueda dentro de cualquier aplicación sólo la utilizaba el 7% de los usuarios, el equipo de Windows decidió cambiar el sistema de búsqueda para mejorarlo y hacerlo más eficiente.

¿Dónde están las Apps?

El nuevo charm de búsqueda ya no muestra una lista de aplicaciones disponibles. En su lugar nos permite buscar dentro de todo el sistema, integrando los resultados del contenido indexado en nuestro equipo con otros datos devueltos por Bing, Xbox Music y otros.
Las aplicaciones antiguas que implementan el contrato de búsqueda de Windows 8 no perderán esa funcionalidad; podremos seleccionar la aplicación para que nos devuelva la página de resultados, pero sólo nos aparecerá esta opción cuando sea la aplicación activa y no estará seleccionada por defecto, pero esto último se puede cambiar.

cookbok search 0

Una vez seleccionada nuestra aplicación en lugar del “Search everywhere” podremos seguir usándola como antes, aunque no tendremos la posibilidad de cambiar a otra aplicación:

Cookbok search

¿Cómo haremos para que se pueda buscar dentro de nuestra aplicación desde cualquier sitio?

Está algo más escondido para nosotros, pero en realidad será mejor que antes, pues parece ser que podremos ofrecer resultados dentro de la búsqueda general. Todavía no está disponible en la preview que podemos probar ahora, si bien en esta charla del Build podemos ver cómo será.
Además, la funcionalidad “Search everywhere” permite encontrar nuestra aplicación si el usuario la tiene instalada y, como me suele ocurrir mucho, no recuerda exactamente el nombre:

search everywhere

¿Y cómo implemento ahora las búsquedas?

Podemos optar por seguir haciéndolo como antes o bien utilizar los nuevos controles y capacidades de búsqueda.

Charm de búsqueda

En el caso de que queramos seguir con la implementación antigua, lo recomendación es que pongamos un botón en la zona superior derecha que abra el charm de búsqueda, así nuestros usuarios sabrán que nuestra aplicación puede buscar. Al ejecutar el siguiente código, el charm estará enfocado en nuestra aplicación en lugar de en el “Search everywhere”, e incluso podemos poner un término de búsqueda por defecto:

Windows.ApplicationModel.Search.SearchPane.getForCurrentView().show("buscar...");

Todavía tenemos una opción mejor, hacer que el cuadro de búsqueda aparezca al empezar a escribir, tal como hacen las aplicaciones de calidad y la pantalla de inicio. El código para añadir esta funcionalidad es muy sencillo:

Windows.ApplicationModel.Search.SearchPane.getForCurrentView().showOnKeyboardInput = true;

El control SearchBox

Si queremos ajustarnos a las nuevas recomendaciones de búsqueda, el equipo de Windows nos lo ha puesto muy fácil. Tenemos un nuevo control de búsqueda que tiene eventos muy parecidos al contrato de búsqueda, pero es un control visual que podemos poner en nuestro UI, tanto para XAML como para HTML:

<div class="pagesearch" data-win-control="WinJS.UI.SearchBox"></div>

Una vez en nuestra página, podremos usar el código que utilizábamos antes en nuestro contrato de búsqueda que utilizaba la consulta:

var search = element.querySelector(".pagesearch").winControl;
search.onquerysubmitted = function (e) {
    console.log(e.detail.queryText);
};

Sólo que ahora tendremos que navegar nosotros a la página de resultados.

También podemos añadir sugerencias de búsqueda:

search.onsuggestionsrequested = function (e) {
    var query = e.detail.queryText.toLowerCase();
    var suggestionCollection = e.detail.searchSuggestionCollection;
    var suggestionList = ["Almería","Albacete", "Alicante"];
    for (var i = 0, len = suggestionList.length; i < len; i++) {
        if (suggestionList[i].substr(0, query.length).toLowerCase() === query) {
            suggestionCollection.appendQuerySuggestion(suggestionList[i]);
        }
    }
};

Activar el foco del control al usar el teclado en la aplicación:

search.winControl.focusOnKeyboardInput = true;

Y ofrecer resultados recomendados con imágenes y separadores para identificarlos del resto, lo que dará un toque de calidad a nuestra aplicación:

search.onsuggestionsrequested = function (e) {

    var queryText = e.detail.queryText,
        query = queryText.toLowerCase(),
        suggestionCollection = e.detail.searchSuggestionCollection;
    var suggestionList = [{
        name: "Almería",
        thumb: "http://upload.wikimedia.org/wikipedia/commons/thumb/4/4e/Bandera_de_Almer%C3%ADa.svg/200px-Bandera_de_Almer%C3%ADa.svg.png"
    },
        {
            name: "Albacete",
            thumb: "http://upload.wikimedia.org/wikipedia/commons/thumb/e/e4/Albacete-Bandera.svg/200px-Albacete-Bandera.svg.png"
        },
        {
            name: "Alicante",
            thumb: "http://upload.wikimedia.org/wikipedia/commons/thumb/2/29/Flag_of_Alicante.svg/200px-Flag_of_Alicante.svg.png"
        }];

    for (var i = 0, len = suggestionList.length; i < len; i++) {
        if (suggestionList[i].name.substr(0, query.length).toLowerCase() === query) {
            if (suggestionCollection.size <2) {
                var reference = Windows.Storage.Streams.RandomAccessStreamReference.createFromUri(
                    new Windows.Foundation.Uri(suggestionList[i].thumb));
                suggestionCollection.appendResultSuggestion(suggestionList[i].name,
                    "El mejor resultado!", "id1", reference, "mejor resultado");
            }
            else {
                if (suggestionCollection.size == 2)
                    suggestionCollection.appendSearchSeparator("Más sugerencias");
                suggestionCollection.appendQuerySuggestion(suggestionList[i].name);
            }
        }
    }
};

sugerencias

Por lo que podemos ver es un control muy completo, que además cachea las últimas búsquedas para ofrecerlas también como sugerencia.

El indexador de Windows

Además del nuevo control de búsqueda, podemos añadir y consultar elementos con el indexador de Windows mediante la clase ContentIndexer. Antes de que os frotéis las manos como hice yo, leed la explicación que aparece en la documentación:

Content is separated per app package, per user, and a package can’t query another package’s data.

Podemos utilizar la potencia del indexador de Windows, pero sólo para y por nuestra aplicación, los datos que añadamos al índice son privados para nuestra aplicación y usuario.

Dicho esto,

  • La clase ContentIndexer nos permite añadir elementos al índice de Windows, de manera que podremos aprovechar la potencia del indexador de Windows dentro de nuestra aplicación. Por ahora el contenido que añadamos al índice será sólo para nuestra aplicación, el resto de las aplicaciones no pueden verlo.
  • También podemos añadir contenido indexado utilizando carpetas locales y creando ficheros appcontent-ms.
  • Con las clases del espacio de nombres Windows.Storage.Search podremos realizar búsquedas dentro del contenido indexado utilizando Advanced Query Syntax, pero sólo del contenido propio.
  • Todavía no sabemos cómo podremos hacer para aparecer dentro de las búsquedas generales de Windows Search.

Conclusiones

Tras estudiar el uso que hacían de la búsqueda los usuarios de las aplicaciones de la tienda Windows, el equipo de Windows se dio cuenta que sistema necesitaba un cambio. Este cambio ha tenido algunas consecuencias pero tenemos la promesa de que va a mejorar. Mientras tanto, ya podemos aprovechar la potencia que nos proporcionan los nuevos controles y funcionalidades del sistema de búsqueda.

Referencias

Anuncios

Formularios interactivos dentro de un ListView

En algunas aplicaciones de la Tienda Windows vemos una colección de elementos dentro de un ListView en las que uno de ellos contiene un formulario con el que podemos interactuar, por ejemplo en esta aplicación, que no sólo tiene uno sino dos:
booking

Vamos a ver cómo podemos crear un interfaz parecido en nuestras aplicaciones HTML5/JavaScript de la Tienda Windows.

Plantilla condicional

Podéis ver en el artículo sobre diseño de listas heterogéneas cómo escribir diferentes plantillas y cargarlas condicionalmente. En nuestro caso vamos a crear una plantilla adicional para el primer elemento de la lista e insertaremos un elemento select, sólo a efectos de demostrar la técnica, sirve con cualquier cosa que tengamos dentro del elemento.

Empezamos con la plantilla de Aplicación de Cuadrícula (Grid App), que ya tiene datos y un control ListView para poder realizar la demostración. Una vez creada, en la página groupedItems.html copiamos la plantilla itemTemplate justo debajo y la modificamos un poco, añadiendo la etiqueta select con tres opciones:

<div class="interactiveitemtemplate itemtemplate" data-win-control="WinJS.Binding.Template">
    <div class="item">
        <img class=" item-image" src="#" data-win-bind="src: backgroundImage; alt: title" />
        <div class="item-overlay">
            <!--<h4 class="item-title" data-win-bind="textContent: title"></h4>-->
            <select>
                <option>Opt 1</option>
                <option>Opt 2</option>
                <option>Opt 3</option>
            </select>
        </div>
    </div>
</div>

Tras añadir la plantilla adicional, podemos modificar la función ready del código de groupedItems.js para que se ejecute la nueva plantilla al crear el primer elemento del ListView (tal como comento en el artículo que he citado antes) y la plantilla general para el resto, sustituyendo el itemTemplate por una función que ejecuta una plantilla u otra según el índice:

var standardTemplate = element.querySelector(".itemtemplate").winControl;
var interactiveTemplate = element.querySelector(".interactiveitemtemplate").winControl;
listView.itemTemplate = function (itemPromise) {
    return itemPromise.then(function (item) {
        var template;
        if (item.index==0) {
            template = interactiveTemplate;
        }
        else {
            template = standardTemplate;
        }
        return template.render(item.data);
    });
};

El modo interactivo: win-interactive

Si ejecutamos el código anterior veremos el primer elemento con la plantilla que hemos aplicado pero no podremos interactuar con el selector. El comportamiento por defecto de los elementos de un ListView es que podamos pulsar sobre el elemento para realizar una navegación. Como nosotros queremos interactuar directamente con el contenido del elemento necesitamos desactivar ese comportamiento por defecto.

En este caso el truco es muy sencillo: basta con añadir la clase win-interactive al elemento que queremos activar, WinJS se encargará del resto de la magia:

<div class="interactiveitemtemplate itemtemplate" data-win-control="WinJS.Binding.Template">
    <div class="item win-interactive">
...

A partir de ahora todo el recuadro se volverá interactivo y dejará de responder como un elemento de ListView. Si queremos que el elemento contenedor siga actuando a la pulsación podemos poner la clase win-interactive sólo en el elemento select … o en el que queramos nosotros.

En otro post ya os contaré más trucos para crear efectos parecidos, pero sin un control ListView.

Cómo usar bien la fórmula calc en CSS3 y la importancia del espacio

Hoy un truco rápido que me ha hecho perder un buen rato en una app de Windows 8, suerte que se me ha ocurrido buscar en MSDN.

A veces vamos a necesitar que un elemento nos ocupe un porcentaje de la página/aplicación, pero dejando libre un cierto margen de pixels.

Hay muchas formas de hacerlo, pero en el caso que tenía hoy necesitaba asignar un margen de 20px y un tamaño del elemento al 100% para que cubriera todo el espacio restante del contenedor.
Las propiedades de estilo margin y padding no se llevan demasiado bien con asignaciones a height y width en formato de porcentaje, el 100% calculado seguirá siendo el total del espacio del contenedor sin el margen. Así que necesitamos realizar un cálculo entre el valor del porcentaje y la cantidad de pixels que estamos utilizando en los margenes. Para esto no hace falta código JavaScript, pues tenemos la función calc que permite hacer exactamente eso en CSS, pero tenemos que ir con mucho cuidado al escribirla, busca las diferencias:

Código erróneo:

.homepage section[role=main] {
    margin-top:20px;
    height:calc(100%-20px);
}

Código correcto:

.homepage section[role=main] {
    margin-top:20px;
    height:calc(100% - 20px);
}

La diferencia es sutil pero importante: entre el 100%, 20px y el símbolo de restar, en un caso no hay espacios y en el código que funciona sí. Es muy fácil olvidar los espacios y si no los ponemos no pasará nada, nada de nada, pues, tal como dicta la norma, un atributo css inválido es ignorado.

Motor de animaciones para aplicaciones HTML (Web + Windows Store)

En artículos anteriores he descrito cómo realizar animaciones de forma óptima utilizando JavaScript y la nueva función requestAnimationFrame. Los ejemplos que daba eran bastante sencillos, pero cuando hacemos una aplicación más grande, como por ejemplo un juego, debemos mover muchos elementos distintos, cada uno con su propia lógica de movimiento.
Para facilitarnos el trabajo y evitar repetir cada vez lo mismo podemos crear un motor de animaciones sencillo y reutilizable.

TL;DR

Este artículo tiene cantidades ingentes de código JavaScript, pero para qué leerlo si puedes ejecutarlo y jugar con él en Fiddle o ver cómo usarlo en una app Windows 8.
El código de ejemplo contiene los siguientes objetos:

  • Animation: un objeto que contiene una lista de elementos a animar y se encarga de gestionar el bucle de animación.
  • Planet y Moon: dos objetos de ejemplo que demuestran cómo se calcula la animación de cada elemento.

Loop Engine

Ahora que ya habéis jugado y destrozado el código os explico qué está haciendo. Para empezar, como quería un código lo más estándar posible, pero que me sea fácil de usar dentro de una aplicación Windows Store, definiré un espacio de nombres sin depender de WinJS:

(function initializeLoop(global) {
    "use strict";

    //espacio de nombres Loop
    global.Loop = global.Loop || {};
})(this);

Una vez tenemos definido nuestro espacio de nombres Loop vamos con el objeto principal del motor.

El objeto Animation

En un alarde de originalidad he definido un objeto llamado Animation. Dicho objeto gestiona el famoso ciclo update + draw dentro de una llamada requestAnimationFrame y además contiene una matriz con los elementos a animar. Así, para que los objetos se animen nos bastará con incorporarlos a la matriz y el objeto se encargará de llamar a las funciones de cálculo y pintado de cada uno.

(function initializeAnimationObject(Loop) {
    "use strict";

    //objeto Animation
    Loop.Animation = function(canvas) {
        this.canvas = canvas;
        this.context2d = canvas.getContext("2d");

        this.animationElements = [];
        this.size = [canvas.width, canvas.height];
        this.delta = 0;

        this._oldTime = undefined;
        this._lastAnimationFrame = undefined;
        this._enabled = false;
        this._paused = false;
    };

El objeto incorpora las funciones start, stop, pause y resume para manejar la animación.

    //empieza la animación
    Loop.Animation.prototype.start = function() {
        if (!this._enabled) {
            this._enabled = true;
            this._loop();
        }
    };

    //para la animación y vuelve a poner el tiempo a 0
    Loop.Animation.prototype.stop = function() {
        this._enabled = false;
        this._paused = false;

        if (this._lastAnimationFrame) {
            cancelAnimationFrame(this._lastAnimationFrame);
        }
        this.animationElements.forEach(function(e) {
            if (e.stop) {
                e.stop();
            }
        });

        this._oldTime = undefined;
        this.update(0);
        this.draw(0);
        this._oldTime = undefined;
    };

    //pausa la animación
    Loop.Animation.prototype.pause = function() {
        if (this._enabled) {
            this._paused = true;
            this.animationElements.forEach(function(e) {
                if (e.pause) {
                    e.pause();
                }
            });
        }
    };

    //continua la animación a partir de la pausa
    Loop.Animation.prototype.resume = function() {
        this._paused = false;
        this.animationElements.forEach(function(e) {
            if (e.resume) {
                e.resume();
            }
        });
    }

La lista de elementos debe contener objetos que pueden tener definidas (o no) las siguientes funciones:

  • update: la función donde se realizan todos los cálculos de posición del elemento
  • draw: en esta función debemos procurar tenerlo todo listo para que sólo tengamos que dibujar
  • pause: sólo la implementamos si tenemos que realizar algo especial al pausar la animación
  • resume: la implementamos en el caso de que debamos reaccionar de alguna manera a la reanudación de la animación.

De esta manera se comunica a los objetos qué deben hacer en cada momento, si es que quieren saberlo.

Desde la función start se llama a una función privada llamada _loop. Esta crea el bucle de llamadas requestAnimationFrame. Como estas llamadas funcionan como un callback, no se ejecutan dentro del contexto del objeto, pero nosotros queremos poder llamar a las funciones this.update y this.draw. El truco utilizado aquí es crear una closure que nos almacene el objeto this dentro de la variable that.

    //Definición de las funciones de Animation
    Loop.Animation.prototype._loop = function() {
        //ciclo de animación básico
        var that = this;
        var insideLoop = function(loopTime) {
            if (that._enabled) {
                that._lastAnimationFrame = requestAnimationFrame(insideLoop);
                that.update(loopTime);
                that.draw(loopTime);
            }
        };
        requestAnimationFrame(insideLoop);
    };

Desde el bucle se llama a las funciones update y draw, que a su vez llaman a las funciones homónimas de los elementos de la lista.

    //update: donde se realizan los cálculos, se llama antes del draw.
    Loop.Animation.prototype.update = function (animationTime) {
        if (animationTime === undefined) {
            animationTime = Date.now();
        }

        if (this._oldTime === undefined) {
            this._oldTime = animationTime;
        }
        this.delta = (animationTime - this._oldTime);
        this._oldTime = animationTime;
        if (!this._paused) {
            this.animationElements.forEach(function (e) {
                if (e.update) {
                    e.update(animationTime);
                }
            });
        }
    };

    //draw: una vez realizados los cálculos se pueden dibujar los elementos en el canvas
    //borra el canvas antes de empezar a pintar el nuevo.
    Loop.Animation.prototype.draw = function (animationTime) {
        var context2d = this.context2d;
        context2d.clearRect(0, 0, this.canvas.width, this.canvas.height);

        this.animationElements.forEach(function (e) {
            if (e.draw) {
                e.draw(animationTime);
            }
        });
        if (this.showFPS) {
            context2d.strokeStyle = '#4f4';
            context2d.strokeText("FPS: " + (Math.round(1000 / this.delta)).toString(), 10, 10);
        }
    };
})(this.Loop);

¿Cómo utilizamos este motor?

Para utilizar el motor necesitaremos por una parte una página HTML con un canvas sobre el que animar:

<canvas style="position:absolute;background-color:black" id="animationCanvas"></canvas>
<div style="position:absolute; top:10px; width:100%">
    <div style="width:100px;margin-left:auto;margin-right:auto;">
        <button class="roundButton" id="playPauseButton" >║</button>
        <button class="roundButton" id="stopButton">■</button>
    </div>
</div>

Y en el script de la página esperaremos a que cargue para llamar a la función start de nuestra animación:

this.onresize = resizeCanvas;
var animation;

function start() {
    resizeCanvas();
    animation = new Loop.Animation(animationCanvas);
    createPlanets(animation);
    animation.start();
}

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

document.addEventListener("visibilitychange", function() {
    animation.pause();
    if (animation.isPaused) {
        playPauseButton.innerHTML = "►";
    }
}, false);

this.onload = function () {
    playPauseButton.onclick = function () {
        if (!animation.isEnabled) {
            animation.start();
            playPauseButton.innerHTML = "&#x2551";
        }
        else if (animation.isPaused) {
            animation.resume();
            playPauseButton.innerHTML = "║";
        }
        else {
            animation.pause();
            playPauseButton.innerHTML = "►";
        }
    };

    stopButton.onclick = function () {
        animation.stop();
        playPauseButton.innerHTML = "►";
    }

    start();
};

Animando una lista de planetas

Una vez tenemos el motor, nos queda añadir los elementos a animar. Vamos a crear una animación de planetas muy sencilla. Para ello necesitaremos definir los objetos Planet con sus métodos update y draw:

(function initializePlanetsNS(global) {
    "use strict";

    //espacio de nombres Loop
    global.Planets = global.Planets || {};
})(this);

(function createPlanetsObjects(Planets) {
    var x = 0,
        y = 1;
    var pi2 = Math.PI * 2;

    Planets.Planet = function(animation) {
        if (animation) {
            this.animation = animation;

            var canvas = this.animation.canvas;
            this.speed = 0;
            this.rotationCenter = [canvas.width / 2, canvas.height / 2];
            this._rotation = 0;
            this.rotationRadius = 0;
            this.position = [this.rotationCenter[x] + this.rotationRadius, this.rotationCenter[y]];
            this.radius = 50;
            this.foreColor = "#FF0";
        }
    }

    Planets.Planet.prototype.update = function(time) {
        var delta = this.animation.delta;
        this._rotation += delta * this.speed / 1000;
        if (Math.abs(this._rotation) > pi2) this._rotation -= pi2 * (this._rotation / this._rotation);

        this.position[x] = this.rotationCenter[x] + this.rotationRadius * Math.cos(this._rotation);
        this.position[y] = this.rotationCenter[y] + this.rotationRadius * Math.sin(this._rotation);
    };
    Planets.Planet.prototype.draw = function(time) {
        var context2d = this.animation.context2d;
        context2d.fillStyle = this.foreColor;
        context2d.beginPath();
        context2d.arc(this.position[x], this.position[y], this.radius, 0, pi2);
        context2d.fill();
    };

    Planets.Planet.prototype.stop = function() {
        this._rotation = 0;
    };

Para darle más vida a la animación vamos a crear también satélites a los planetas. En este caso la función de draw no hace falta definirla, reaprovechamos la del objeto Planet:


    Planets.Moon = function(animation, planet) {
        Planets.Planet.apply(this, arguments);
        this.planet = planet;
    }
    Planets.Moon.prototype = new Planets.Planet();

    //store the update from the Planet object to avoid repeating code
    Planets.Moon.prototype.updateBase = Planets.Moon.prototype.update;

    //change update function to take planet position as rotation center
    Planets.Moon.prototype.update = function(time) {
        this.rotationCenter = this.planet.position;
        this.updateBase(time);
    };
})(this.Planets);

Y en el script principal creamos los planetas con sus lunas:

function createPlanets(animation) {
    var sun = new Planets.Planet(animation);
    sun.radius = 40;
    animation.animationElements.push(sun);
    for (var i = 0; i < 1+rnd(8,2); i++) {
        var planet = new Planets.Planet(animation);
        planet.rotationCenter = [animationCanvas.width / 2, animationCanvas.height / 2];
        planet.radius = 10 + rnd(20);
        planet.speed = rnd(90, 1) * Math.PI * 2 / 360;
        planet.rotationRadius = sun.radius * 2 + i * 150 + rnd(100);
        planet.foreColor = "rgb(" + rnd(255) + "," + rnd(255) + "," + rnd(255) + ")";
        animation.animationElements.push(planet);

        for (var m = 0; m < rnd(5); m++) {
            var moon = new Planets.Moon(animation, planet);
            moon.radius = 5 + rnd(planet.radius / 3);
            moon.speed = rnd(180, 1) * Math.PI * 2 / 360 * (Math.random() > 0.5 ? -1 : 1);
            moon.rotationRadius = planet.radius + moon.radius + 10 + rnd(40);
            moon.foreColor = "rgb(" +
                (50 + rnd(205)) + "," +
                (50 + rnd(205)) + "," +
                (50 + rnd(205)) + ")";
            animation.animationElements.push(moon);
        }
    }
}

function rnd(max, min) {
    if (!min)
        min = 0;
    return Math.round(min+( Math.random() * (max == undefined ? 1 : (max-min))));
}

Windows 8

El código anterior funcionará directamente en una aplicación Windows Store, pero para mejorar un poco su funcionamiento podemos aplicar algunos trucos como la reacción a los eventos de suspensión para pausar la animación.

Ya estamos preparados para probarlo en un navegador moderno o en una app Windows 8.

Nota: este ejercicio es sólo un ejemplo, no es un motor optimizado ni preparado para funcionar bien en todos los navegadores. Aún así funciona en la mayoría de navegadores, contiene código fallback para los navegadores que no dispongan todavía de la versión definitiva de requestAnimationFrame e incluso funciona en algunos navegadores móviles.

Conclusiones

Hemos visto que gracias a las nuevas características de HTML5 y JavaScript podemos crear animaciones suaves y definir un modelo de objetos que nos permita crear objetos que controlan su propio movimiento.
El ejemplo funciona tanto en navegadores como dentro de una aplicación Windows Store de Windows 8.

Algunos ejemplos de HTML y JavaScript para aplicaciones Windows Store

doubts...
He renovado la página de inicio de los ejemplos que escribo para resolver las dudas que me vais pidiendo sobre Windows 8. Como algunos sabréis estoy utilizando codeplex para que estén disponibles para todo el mundo. Los ejemplos que hay hasta ahora son los siguientes:

  • Listas heterogéneas: un ejemplo de cómo representar listas con elementos de diferentes tamaños. Explicado en el artículo del mismo nombre en desarrolloweb.com.
  • Integración Google Maps: cómo utilizar las vistas seguras de HTML para poder incluir código dinámico externo a nuestras aplicaciones. También en desarrolloweb.com.
  • PaintJS: una aplicación táctil para pintar sobre un elemento <canvas>, el resultado acabó siendo una aplicación Windows Store: Finger Paint. Encontraréis una introducción en mi entrada del blog sobreeventos táctiles.
  • Ejemplo Timer: cómo crear un contador que no se pare cuando nuestra aplicación no está en marcha, también en el Windows Store: Big Stopwatch. La explicación en el artículo sobre la Cuenta Atrás.
  • Planets (en progreso): ejemplo de uso de un motor de animaciones propio (Loop).

Estoy preparando unos cuantos ejemplos más y pronto actualizaré los que hayan quedado anticuados. Os mantendré informados.

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.

Tipos MIME en IIS Express / WebMatrix

Si habéis probado WebMatrix sabréis que es una herramienta muy útil y práctica cuando todo funciona bien. Hace poco quise hacer una prueba con una fuente WOFF y no había manera de hacerla funcionar, aunque por mucho que mirara el código no encontraba qué estaba haciendo mal…

<style type="text/css">
    @font-face
    {
        font-family: SegoeCustom;
        src:  url('fonts/segoe.woff') format('woff');
        font-weight: normal;
        font-style: normal;        
    }
    p {
        font-family:SegoeCustom, sans-serif;
        font-size:100px;
        }
</style>

Lo que pasaba es que estaba haciendo la prueba en WebMatrix y los tipos MIME admitidos son sólo los más comunes, aquí vemos el resultado en la lista de requests:

Podemos intentar modificar los tipos MIME con el IIS Manager, pero en IIS 8 no he conseguido encontrarlos, los habrán cambiado de sitio otra vez :). Menos mal que tenemos una manera mucho más sencilla de controlar los tipos MIME a nivel de aplicación web, desde el archivo web.config de la siguiente manera:

<?xml version="1.0"?>
<configuration>
  <system.webServer>
      <staticContent>
        <mimeMap fileExtension=".woff" mimeType="font/x-woff" />
      </staticContent>
  </system.webServer>
</configuration>

IIS Express nos permite añadir los tipos directamente en el web.config, lo que nos será muy útil en WebMatrix y también en VS2010 cuando estemos usando IIS Express.

Podéis encontrar más detalles en el blog de msdn.