Etiquetado: JavaScript

Filtros REST $filter con texto no ASCII desde JavaScript

Los servicios OData son muy cómodos pues se pueden consumir desde casi cualquier parte y tecnología. Consumir estos datos desde cualquier aplicación HTML/JavaScript, incluso desde las aplicaciones de Windows 8, es muy sencillo pues nos basta con utilizar las llamadas HTTP de toda la vida para interactuar con el servicio.

Desde las consultas REST podemos filtrar utilizando el parámetro $filter en la url, así podemos consultar el catálogo de películas de Netflix y filtrar para que sólo nos dé las series:
http://odata.netflix.com/v2/Catalog/Titles?$filter=Type eq ‘Series’

En la lengua de Shakespeare no tendremos ningún problema, pero si queremos filtrar con caracteres no ASCII tendremos comportamientos que no serán los que nos esperábamos. Por ejemplo, filtramos por el nombre José dentro del mismo servicio:
http://odata.netflix.com/v2/Catalog/Titles?$filter=substringof(‘José’,Name)
Y obtenemos resultados como este, donde el nombre José no aparece:
netflix_nonasciifilter
De hecho, dependiendo del servicio y desde dónde lo llamemos obtendremos resultados diferentes, pues la interpretación de los caracteres que recibe se puede hacer de varias formas.

La solución está en codificar el filtro dentro de la url, para codificar el texto la función adecuada en JavaScript es encodeURIComponent, pues la función escape produce también resultados no deseados. Podéis probar los ejemplos en jsfiddle, aunque al ser JSONP también produce efectos diferentes según como codifiquemos; podemos comparar el jsfiddle con el resultado de consultar directamente las urls:

En Windows 8 tendremos el mismo problema y lo resolveremos igual, utilizando la función encodeURIComponent:

var query = "José";
var encoded = encodeURIComponent(query);
WinJS.xhr({
    url: "http://odata.netflix.com/v2/Catalog/Titles?$filter=substringof('" +
        encoded + "',Name)&$format=json"
}).then(function (xhr) {
    var result = JSON.parse(xhr.responseText);
    var results = result.d.results;
    // a partir de aquí utilizamos los datos
    var body=document.querySelector("body");
    results.forEach(function (r) {
        var div=document.createElement("div");
        div.textContent=r.Name;
        body.appendChild(div);
    });
});

Antes de acabar, recordad que para enviar la consulta también deberíamos comprobar si el texto a buscar contiene el carácter de comilla simple (‘), pues deberemos sustituirlo por la misma comilla escrita dos veces seguidas (”).
Solucionado con un poco de regex:

query=query.replace(/'/g, "''")

Anuncios

La funcionalidad deshacer y repetir en JavaScript para Windows 8

Undo button
Los participantes de un hackathon tienen que poner todo su empeño y mucho más para conseguir en pocos días crear una aplicación brillante; eso les obliga a buscar soluciones para problemas que no se habían planteado nunca antes. Durante los hackathones, megathones y demás formaciones que he ido dando sobre Windows 8 suelen surgir dudas más profundas que en un curso estándar sobre algún lenguaje o tecnología concretos.

Una de las cuestiones más recurrentes suele ser la funcionalidad deshacer en una aplicación, ya sea una calculadora o un programa de dibujo. En este artículo voy a intentar solucionaros esa duda con un ejemplo de código.

Descarga el código fuente de esta aplicación desde codeplex

Patrones de diseño

Como no me gusta reinventar la rueda vamos a hacer uso de uno de los dos patrones de diseño del GoF que se suelen utilizar en estos casos, a saber:

  • Memento: nos permite almacenar el estado de un objeto, respetando la encapsulación del mismo, para poder recuperarlo después.
  • Command: encapsula una petición como un objeto, con el método y los parámetros que va a utilizar sobre el receptor de la petición. Esto nos permite crear una cola de solicitudes y permitir el retroceso de operaciones (siempre que la operación lo permita).

Dependiendo del caso utilizaremos un método u otro. Por ejemplo, en el caso de un programa de dibujo sobre lienzo el Memento nos puede resultar más fácil de implementar, pues cada vez que se dibuje un elemento podemos guardar una foto del mismo, o una porción para optimizar memoria. En el caso de un procesador de textos el patrón Command seguramente será mucho más óptimo, pero cada comando tendrá que implementar su propia función deshacer.

Para hacer un ejercicio sencillo vamos a basarnos en el patrón memento para desarrollar un ejemplo.

El patrón Memento en JavaScript

En el patrón Memento utilizamos tres objetos:

  • Memento: es el almacén del estado interno del objeto, guarda la información necesaria para recuperar el estado del objeto a un momento concreto
  • Originator: crea el Memento que contiene la instantánea de su estado interno y sabe restaurar ese estado usando el Memento
  • Caretaker: gestiona la lista de Mementos, pero no utiliza nunca la información del Memento directamente.

Para demostrar el uso del patrón utilizaremos una aplicación de pintado en un canvas ya utilizada en este blog para tratar los eventos táctiles.

Empezaremos por definir un espacio de nombres para evitarnos problemas en JavaScript y definiremos el objeto básico Memento:

(function mementoDefinition(global) {
    global.CanvasState = global.CanvasState || {};

    ///Memento
    CanvasState.memento = function (state) {
        this._state = state;
    };
    Object.defineProperty(CanvasState.memento.prototype, "state", {
        get: function () { return this._state; }
    });
    ///...

Podréis observar que es extremadamente sencillo: un constructor con el estado y una propiedad para leer el estado. No es modificable ni tiene ninguna funcionalidad, sólo almacena información.

El siguiente paso es crear el objeto Caretaker, el que se encarga de almacenar la lista de objetos Memento; necesitaremos un Array para almacenar los estados de deshacer y otro para los estados de rehacer.

En esta implementación ponemos un parámetro de límite, para evitar llenar toda la memoria con las operaciones de undo. Para gestionar los estados tenemos las funciones addMemento, que nos permite ir añadiendo estados a la lista y las funciones getUndoMemento y getRedoMemento para recuperar los estados, dependiendo de si queremos deshacer una operación o volver a hacerla desde el historial.

///Caretaker constructor
CanvasState.caretaker = function (maxLevels) {
    this._undoStates = [];
    this._redoStates = [];
    this._maxLevels = maxLevels;
};

CanvasState.caretaker.prototype.addMemento = function (memento) {
    this._undoStates.push(memento);
    this._redoStates = [];
    if (this._undoStates.length > this._maxLevels) {
        this._undoStates.splice(0, 1);
    }
};

CanvasState.caretaker.prototype._canUndo = function () {
    return this._undoStates.length > 1;
}
CanvasState.caretaker.prototype._canRedo = function () {
    return this._redoStates.length > 0;
}

CanvasState.caretaker.prototype.getUndoMemento = function () {
    if (this._canUndo()) {
        var state = this._undoStates.pop();
        this._redoStates.push(state);
        return this._undoStates[this._undoStates.length - 1];
    }
    else
        throw "Undo not allowed, states array empty";
};
CanvasState.caretaker.prototype.getRedoMemento = function () {
    if (this._canRedo()) {
        var state = this._redoStates.pop();
        this._undoStates.push(state);
        return state;
    }
    else
        throw "Redo not allowed, states array empty";
}

Object.defineProperty(CanvasState.caretaker.prototype, "canUndo", {
    get: function () { return this._canUndo(); }
});
Object.defineProperty(CanvasState.caretaker.prototype, "canRedo", {
    get: function () { return this._canRedo(); }
});

Por ahora, con estos dos objetos no hemos realizado ninguna operación con el objeto canvas, sólo tenemos el objeto que almacena el estado y el que se encarga de mantenerlo en memoria.

El tercer y último objeto es el Originator, donde ocurren todas las operaciones. Este objeto sabe cómo crear un Memento y cómo recuperar el estado a partir del mismo. En el ejemplo vamos a utilizar el objeto canvas para recuperar una imagen y la guardaremos dentro de un Memento que podremos utilizar más adelante para restaurar el estado.

    ///Originator
    CanvasState.originator = function (canvas) {
        this._canvas = canvas;
        this._ctx = canvas.getContext("2d");
    };
    CanvasState.originator.prototype.saveToMemento = function (x, y, w, h) {
        if (x === undefined)
            x = 0;
        if (y === undefined)
            y = 0;
        if (w === undefined)
            w = this._canvas.width;
        if (h === undefined)
            h = this._canvas.height;

        var img = this._ctx.getImageData(x, y, w, h);
        return new CanvasState.memento({ image: img, x: x, y: y, w: w, h: h });
    };
    CanvasState.originator.prototype.restoreFromMemento = function (memento) {
        this._ctx.putImageData(memento.state.image, memento.state.x, memento.state.y);
    };

})(this);

Y así cerramos la función auto-ejecutable que define todos nuestros objetos. Todo el código anterior lo podemos meter en un archivo llamado memento.js y así lo podremos reutilizar fácilmente.

Como podréis observar, en todo el código el único objeto que sabe algo del canvas es el objeto originator.

Aplicación del patrón a un caso real

Ahora que tenemos todos los objetos preparados, vamos a utilizarlos en la aplicación que hicimos en un post anterior sobre los eventos táctiles. Para demostrar los eventos pintábamos sobre un canvas y ahora vamos a ir guardando el estado para poder recuperarlo más tarde.

Para empezar, en default.js añadimos unas variables que vamos a utilizar para la nueva funcionalidad, justo debajo de las declaraciones del canvas y el ctx:

var originator, caretaker;

Seguimos en default.js, tras los eventos táctiles dentro de app.onactivated, crearemos los objetos originator y un caretaker de 20 niveles de deshacer:

// creamos el originator sobre nuestro canvas
// y un caretaker de 20 niveles
originator = new CanvasState.originator(canvas);
caretaker = new CanvasState.caretaker(20);

//lo primero que haremos será añadir el canvas vacío como memento.
caretaker.addMemento(originator.saveToMemento());

La función startDrawing quedará exactamente igual, donde vamos a guardar el estado es al levantar el dedo de la pantalla, en la función stopDrawing, que quedará así:

function stopDrawing(e) {
    doDraw = false;
    caretaker.addMemento(originator.saveToMemento(0, 0, canvas.width, canvas.height));
}

Ahora guardamos los cambios cada vez que levantamos el dedo de la pantalla. Nos falta poder recuperarlos. Para ello vamos a añadir una barra de botones en la página default.html justo debajo del canvas:

<div id="appBar" data-win-control="WinJS.UI.AppBar" data-win-options="{sticky:true}">
    <button data-win-control="WinJS.UI.AppBarCommand" 
        data-win-options="{id:'cmdUndo',label:'Undo',icon:'undo',section:'global'}">
    </button>
    <button data-win-control="WinJS.UI.AppBarCommand" 
        data-win-options="{id:'cmdRedo',label:'Redo',icon:'redo',section:'global'}">
    </button>
</div>

La marcamos como sticky para que no desaparezca mientras pintamos y nos resulte más cómoda de utilizar. Añadimos dos manejadores de evento en default.js, después de las líneas que crean el originator y el caretaker:

document.getElementById('cmdUndo').winControl.addEventListener("click", function () {
   if (caretaker.canUndo)
      originator.restoreFromMemento(caretaker.getUndoMemento());
});

document.getElementById('cmdRedo').winControl.addEventListener("click", function () {
   if (caretaker.canRedo)
      originator.restoreFromMemento(caretaker.getRedoMemento());
});

Vuestro turno

Está claro que a este código le queda mucho margen de mejora, así que hoy os voy a poner deberes:

  • Intentad mejorar el dibujado cuando hay múltiples puntos táctiles simultáneos
  • Para mejorar el rendimiento y la gestión de memoria de la app, deberíamos almacenar sólo la zona que se ha modificado del canvas en lugar de guardarlo todo. ¿Os atrevéis?

Descarga el código fuente de esta aplicación desde codeplex

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.

Reaccionar antes de la suspensión en apps Windows 8 JavaScript

Game OverCuando desarrollamos una aplicación para Windows 8 debemos tener en cuenta los eventos de suspensión y resumen para que nuestra aplicación reaccione correctamente. Al principio, el evento de suspensión nos parecerá un buen candidato para guardar dónde estaba el usuario antes de suspender.

En muchas aplicaciones será así, pero en algunas nos causará algunos problemas que puede que nos pasen desapercibidos, pero no a nuestros usuarios.

Pongamos un pequeño ejemplo: estamos haciendo un juego de zombis hambrientos; durante el mismo, los zombis se nos van acercando peligrosamente. Si por cualquier motivo el jugador cambia de aplicación durante el juego, nosotros, que nos hemos leído bien los requisitos del Windows Store, pondremos el juego en pausa cuando ocurra el evento de suspensión…

La suspensión no es determinista

  • ¿Qué ocurrirá? Cuando el jugador vuelva a su partida habrá perdido porque los zombis ya se lo habrán comido.
  • ¿Por qué? Porque la aplicación habrá continuado en ejecución durante 10 segundos sin que el usuario la vea, suficiente para que el zombi llegue hasta el jugador.
  • ¿Cómo? La suspensión no es de efecto inmediato, no es determinista, ocurre en una ventana de aproximadamente 10 segundos desde que la aplicación no está visible.

Lo que debemos hacer en estos casos es utilizar el evento de cambio de visibilidad para realizar las operaciones necesarias. En las aplicaciones XAML tenemos el evento Window.Current.VisibilityChanged, en JavaScript el evento está en el elemento document y lo encontraremos como visibilitychange.

Para probarlo, creamos una aplicación JavaScript vacía y añadimos el código que sigue. Podremos ver cómo y cuándo se ejecutan estos eventos, pues iremos lanzando el resultado de los eventos por la pantalla:

(function () {
    "use strict";

    WinJS.Binding.optimizeBindingReferences = true;

    var app = WinJS.Application;
    var activation = Windows.ApplicationModel.Activation;

    function log(str) {
        var body = document.body;
        function format(txt) {
            return new Date() + ": " + txt + "<br/>";
        }
        body.innerHTML = format(str);
        log = function (txt) {
            body.insertAdjacentHTML("beforeEnd", format(txt));
        };
    }

    app.onactivated = function (args) {
        if (args.detail.kind === activation.ActivationKind.launch) {
            if (args.detail.previousExecutionState !== activation.ApplicationExecutionState.terminated) {
            } else {
            }
            args.setPromise(WinJS.UI.processAll());
            
            document.addEventListener("visibilitychange", function (e) {
                log("La visibilidad cambió a " + document.visibilityState);
            });
        }
    };

    app.oncheckpoint = function (args) {
        log("La aplicación se suspendió");
    };

    app.start();
})();

Y aquí podéis ver el resultado, entre que se esconde la aplicación y ésta pasa a modo “hidden” pasan más de 10 segundos:

Eventos táctiles en aplicaciones Metro HTML5

Touch the Sun - IMG_9463_72dpi Los eventos táctiles en Windows 8 son muy fáciles de manejar y desde HTML5 tenemos un modo muy directo de hacerlo a través de tres eventos que nos permitirán manejar ratón, lápiz y dispositivo táctil a la vez y de una manera muy sencilla.

Estos son: MSPointerDown, MSPointerUp y MSPointerMove que podremos aplicar a cualquier elemento visual.

Para ver cómo funciona vamos a hacer una aplicación de dibujo muy básica con un canvas sobre el que podremos pintar con los dedos, con un dispositivo tipo lápiz o con el ratón. Colocamos un elemento canvas en el default.html de una aplicación Metro:

<canvas id="demoCanvas"></canvas>

Una vez tengamos el canvas, tendremos que utilizar esos eventos. En el onactivated de nuestra página (en default.js) vamos a añadir código a los eventos que he comentado antes:

var demoCanvas;
var ctx;

app.onactivated = function (eventObject) {
    if (eventObject.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.launch) {
        if (eventObject.detail.previousExecutionState !==
            Windows.ApplicationModel.Activation.ApplicationExecutionState.terminated) {
            // TODO: This application has been newly launched. Initialize
            // your application here.
        } else {
            // TODO: This application has been reactivated from suspension.
            // Restore application state here.
        }
        WinJS.UI.processAll();

        demoCanvas = document.querySelector("#demoCanvas");
        //ponemos el canvas a fullscreen
        demoCanvas.width = document.body.clientWidth;
        demoCanvas.height = document.body.clientHeight;

        //obtenemos el context para pintar
        ctx = demoCanvas.getContext("2d");

        //usamos los tres eventos para activar el pincel y pintar
        demoCanvas.addEventListener("MSPointerDown", startDrawing);
        demoCanvas.addEventListener("MSPointerUp", stopDrawing);
        demoCanvas.addEventListener("MSPointerMove", draw);
    }
};

Una vez tenemos los eventos, vamos a escribir esos tres métodos para pintar en el canvas. Las funciones recibirán un parámetro con la información del evento.
En la función startDrawing vamos a recuperar qué tipo de puntero estamos utilizando (ratón, lápiz o táctil) para cambiar el grosor del trazo:

var doDraw = false;
var pointerDeviceType;

function startDrawing(e) {
    doDraw = true;
    var pointer = Windows.UI.Input.PointerPoint.getCurrentPoint(e.pointerId)
    pointerDeviceType= pointer.pointerDevice.pointerDeviceType;
    draw(e);
}

En el evento stopDrawing simplemente deshabilitamos el trazo, para evitar que vaya pintando cuando movemos el ratón:

function stopDrawing(e) {
    doDraw = false;
}

Y en el método draw pintaremos con trazos diferentes según el dispositivo y colores diferentes según el identificador del puntero, para poder identificar múltiples inputs simultáneos:

function draw(e) {
    if (doDraw) {
        //cambiamos entre tres colores según el id del puntero
        var id = e.pointerId % 3;
        var rgb = [0, 0, 0];
        rgb[id] = 255;

        //cambiamos el grosor según el puntero (ratón=fino, lápiz=mediano, táctil=grueso)
        var width = 10;
        switch (pointerDeviceType) {
            case Windows.Devices.Input.PointerDeviceType.mouse:
                width = 10;
            case Windows.Devices.Input.PointerDeviceType.pen:
                width = 20;
                break;
            case Windows.Devices.Input.PointerDeviceType.touch:
                width = 40;
        }

        //pintamos un círculo en el contexto 2d en la posición del movimiento
        ctx.beginPath();
        ctx.arc(e.clientX, e.clientY, width, 0, 2 * Math.PI, false);
        ctx.fillStyle = "rgb(" + rgb.join(",") + ")";
        ctx.fill();
    }
}

Aquí podemos ver el resultado en el simulador de Windows 8:

Puedes descargar el código de ejemplo en Codeplex.

Cuenta Atrás en Windows 8

Descarga el código del ejemplo en codeplex.

El pasado día 22 de Marzo se realizó en el ParcBit, organizado por el grupo de usuarios Netsaimada, un gran evento sobre ALM donde participaban Luis Fraile y Rodrigo Corral. Aproveché el evento, ya que me dejaron “colarme”, para explicar el desarrollo de aplicaciones Metro en Windows 8.
En los eventos suele ocurrir que el ponente tiene más cosas que contar que tiempo en una sesión. Para evitar que las sesiones se vayan comiendo el tiempo de las siguientes, suele haber alguien de la organización que avisa al ponente cuando está agotando el tiempo.
Si esa persona soy yo es un desastre, porque se me olvida mirar el reloj, pierdo el papel con el horario de las sesiones y otras cosas por el estilo. Así que Darío me propuso hacer un cronómetro para una tableta Acer Iconia que traía con Windows 8 y pensé que no me costaría mucho desarrollar una aplicación sencilla con una cuenta atrás y un botón de iniciar y parar.




Es una aplicación hecha en pocos minutos y sin muchas pretensiones, pero nos servirá para demostrar unas cuantas características interesantes del Windows 8.

¿Qué características vamos a usar?

Para desarrollar una aplicación así tenemos que tener en cuenta unas cuantas cosas:

  • Las aplicaciones metro sólo se están ejecutando mientras están en primer plano
  • Cuando una aplicación metro pasa a segundo plano puede ocurrir que se cierre completamente
  • Las aplicaciones metro no pueden instalar nada en el sistema, todo lo que utilice la aplicación debe desplegarse con la misma. Si queremos usar una fuente de letras personalizada la debemos distribuir con la aplicación
  • Para mejorar la usabilidad de nuestra aplicación, esta debe adaptarse a los diferentes tamaños de pantalla. Añadiendo una vista acoplada podremos conseguir que nuestra aplicación sea utilizada durante más tiempo

El desarrollo

Como es una aplicación muy sencilla y queremos comprobar cómo de fácil es crear una aplicación en HTML5/Javascript, vamos a crear una aplicación con la plantilla más sencilla, la Blank Application:



Una vez creado el proyecto, vamos a programar todo en los ficheros default.html, default.js y default.css. Lo primero vamos a crear el contenedor para los dígitos, el cuadro para introducir el tiempo y los botones de start y stop dentro del body:

<body>
    <div id="timer">
        <p class="timer-time" id="time">00:00:00</p>
        <div class="timer-config" style="height: 228px;">
            <label for="length">Minutes:</label>
            <input type="number" id="length" min="1" value="60" />
            <button>Start</button>
            <button>Stop</button>
        </div>
    </div>
</body>

Y ahora podemos programar nuestro reloj, primero nos creamos una clase que nos permita inicializar un timer a una hora en concreto y saber cuánto tiempo ha pasado desde entonces. Como no tengo demasiada experiencia con JavaScript, he buscado un poco por allí y por allá y con algunos retazos de lo que me he encontrado me he creado mi propio objeto:

var downTimer = {
    time: 0,
    length: 0,
    now: function () { return (new Date()).getTime(); },
    start: function (length) { this.time = this.now(); this.length = length*1000; },
    since: function () { return this.now() - this.time; },
    last: function () {
        var l = this.length - this.since();
        if (l < 0) return this.msToTime(0);
        else
            return this.msToTime(l);
    },
    stop: function () { this.time = this.length = 0; },
    msToTime: function (s) {
            var ms = s % 1000;
            s = (s - ms) / 1000;
            var secs = s % 60;
            s = (s - secs) / 60;
            var mins = s % 60;
            var hrs = (s - mins) / 60;
            return (hrs<10?'0':'')+ hrs + ':' + (mins<10?'0':'') + mins + ':' + (secs<10?'0':'') + secs;
        }
}

Como en Javascript no tenemos clases, pero es un lenguaje dinámico, podemos crear objetos complejos sin complicarnos demasiado. En el caso anterior es un objeto singleton donde declaramos directamente las variables y funciones que vamos a utilizar. Las importantes en este objeto son las funciones start, stop y last que son las que harán todo el trabajo.

Ahora, dentro del evento app.onactivated, llamaremos a un timeout de javascript para que ejecute de manera cíclica un método de refresco para nuestro contador, cada 33 milisegundos para simular un ciclo de 30 frames por segundo, aunque con 12 o incluso 3 frames nos bastaría:

//execute at 30fps (enough)
setInterval(refresh, 33);

Definiremos la función refresh para que refresque el elemento time con el contenido de nuestra clase downtimer:

function refresh() {
    var element = document.getElementById("time");
    element.textContent = downTimer.last();
}

Para que nuestra cuenta atrás funcione sólo nos falta que los botones de start y stop tengan funcionalidad. Para hacerlo más elegante definiremos dos métodos en un espacio de nombres y así los podremos llamar bien desde la página:

WinJS.Namespace.define("defaultPage",{
    startClick: function () {
        var length = document.getElementById("length").value;
        downTimer.start(length * 60);
    },
    stopClick: function () {
        downTimer.stop();
    }
});

Una vez tenemos las funciones dentro de un espacio de nombres modificamos los botones en el default.html para que al pulsarlos se llame a las funciones que hemos definido:

<button onclick="defaultPage.startClick()">Start</button>
<button onclick="defaultPage.stopClick()">Stop</button>

Probamos la aplicación así y ya tenemos una cuenta atrás funcionado:



Aunque la pantalla nos queda algo vacía, vamos a darle un poco de estilo.

Cambiar el UI con CSS

Hasta ahora nos hemos preocupado sólo por la funcionalidad, vamos a cambiar el aspecto con unos pocos trucos de css.
Vamos primero a por el contador, me gustaría más una fuente estilo reloj digital, pero no podemos asegurarnos que el Windows 8 donde se instale la aplicación vaya a tenerla instalada, así que tendremos que encontrar una fuente que podamos distribuir con nuestra aplicación. Como estamos haciendo una aplicación HTML, lo más fácil es encontrar alguna fuente gratuita, convertirla en formato woff y distribuirla con la aplicación. Para ello añadimos la fuente a una carpeta del proyecto:

Y una vez allí, la configuramos en el default.css:

@font-face {
    font-family: 'dsdigital';
    src: url('/fonts/dsdigital.woff') format('woff');
    font-weight: normal;
    font-style: normal;
}

#timer .timer-time {
        font-family: 'dsdigital';
        font-size: 190px;        
        text-align: center;
    }

Ahora ya empieza a parecer algo más serio:



Sólo nos queda darle algo más de estilo al cuadro de texto y a los botones para que sean más adecuados para utilizar de forma táctil. Un poco más de CSS, utilizar el id de los elementos dentro del CSS no es muy adecuado, pero para hacer una demo rápida nos valdrá:

#timer .timer-config {
    position: relative;
    width: 580px;
    margin-left: auto;
    margin-right: auto;
    display: block;        
}
#timer label {
            font-size: 1.45em;
}
#timer input {
    border: #000 1px;
    height: 2em;
    font-size: 2.45em;
    text-align: right;
}
#timer button {
        width: 2em;
        background: #000;
        color: #eee8d2;
        border: #fff 1px;
        height: 2em;
            font-size: 2.45em;
            font-weight: 300;
    }

 #timer button:active {
            background: #00f;
        }

Otra cosa que me gustaría es que los botones tengan un glyph en lugar de un texto, para que se entienda en cualquier idioma. Una primera idea sería poner una imagen, pero para no tener problemas con el escalado podemos usar algunos caracteres Unicode como el Black Rectangle (25A0) y el Black Right-Pointing Pointer (25B6):

<button onclick="defaultPage.startClick()">▶</button>
<button onclick="defaultPage.stopClick()">■</button>

Y así ya tendremos la aplicación básica.

Vista Acoplada

Una de las características de las aplicaciones Metro en Windows 8 es que son a pantalla completa, pero en los dispositivos que tengan la resolución suficiente el usuario puede tener una vista acoplada a un lado de la aplicación. Esto nos ayudará a que los usuarios utilicen nuestra aplicación durante más tiempo.
En nuestro caso, crear la vista acoplada no necesitará de código javascript, nos bastará con adaptar la hoja de estilos para la que la vista quede más compacta. En la misma hoja de estilos de la página tenemos unas reglas para las diferentes vistas, editamos la vista snapped así:

@media screen and (-ms-view-state: snapped) {
    body {
        font-size: 10px;
    }
    #timer .timer-time {
        font-size: 50px;
    }
    #timer .timer-config {
        height: 230px;
        width: 315px;
        margin: 5px;
        padding: 5px;
        text-align: center;
    }
}

Y ya tenemos una vista ajustada a la ventana acoplada:


Itinerancia de datos

Ahora que nuestra aplicación ya tiene el contador, funciona y se ve bonita, le podemos añadir muy fácilmente una pequeña funcionalidad que nos vendrá bien cuando paremos nuestra aplicación o utilicemos la aplicación en varios dispositivos. Podemos guardar la información importante de nuestro temporizador en la configuración de itinerancia, que se copiará automáticamente entre los diferentes dispositivos que tengamos con Windows 8 y utilicemos con la misma cuenta LiveID.

var appdata = Windows.Storage.ApplicationData;

WinJS.Namespace.define("defaultPage",{
    startClick: function () {
        var length = document.getElementById("length").value;
        downTimer.start(length * 60);
        //store roaming
        appdata.current.roamingSettings.values["time"] = downTimer.time;
        appdata.current.roamingSettings.values["length"] = downTimer.length;
    },
    stopClick: function () {
        downTimer.stop();
        //clear roaming
        appdata.current.roamingSettings.values["time"] = null;
        appdata.current.roamingSettings.values["length"] = null;
    }
});

Ahora sólo nos queda leer el dato al arrancar la aplicación en el evento app.onactivated:

app.onactivated = function (eventObject) {
    if (eventObject.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.launch) {
        var time = appdata.current.roamingSettings.values["time"];
        if (time) {
            downTimer.time = time;
            downTimer.length = appdata.current.roamingSettings.values["length"];
        }
...

Al activar la aplicación leemos la configuración, si existe, y así nuestro contador seguirá como si nada hubiera pasado.
Este ejemplo costó menos de media hora de desarrollo, me ha costado muchísimo más describirlo que hacerlo 🙂

¿Qué hemos visto?

  • Algunas características que nos proporciona Windows 8, como la vista acoplada y la configuración de itinerancia, para dar un punto de calidad a nuestra aplicación, de manera muy sencilla pues Windows 8 nos proporciona la mayor parte de la funcionalidad “out of the box”.
  • La creación de espacios de nombres (namespaces) nos ayuda a organizar el código de nuestra aplicación, y nos facilita la escritura de código gracias al intellisense.
  • La integración del código HTML5/JavaScript estándar con el código específico de la plataforma Windows 8
  • Cómo incorporar fuentes de letras con nuestra aplicación
  • Uso de caracteres especiales Unicode para tener glyphs escalables
  • La separación del diseño y la lógica del interfaz
  • Las media queries de css para adaptar la interfaz a los diferentes tamaños y orientaciones del dispositivo

Y todo esto sólo con un cronómetro, imaginad lo que podéis llegar a hacer con vuestras aplicaciones.

Descarga el código del ejemplo en codeplex.