Categoría: Tutoriales

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

Anuncios

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.

Hackathones, Megathones y contenidos

Hace casi dos meses que no he escrito nada en mi blog, pero no significa que haya dejado de escribir y de hacer cosas, sino todo lo contrario.

Estuvimos hasta arriba organizando el Hackathon Windows 8 “Con 8 Basta” de Palma. Para los asistentes a los hackathones escribí un tutorial de aplicaciones Metro en JavaScript que está disponible como material de formación en la web del Megathon Windows 8, consta de 13 capítulos, pero prometo más en breve. Por ahora os dejo dos anexos en el sitio Desarrollo Web: contenido web dinámico en aplicaciones metro y este otro sobre listas heterogéneas en Windows 8.

Recordad que se acerca el Megathon Windows 8 y en Palma habrá un gran movimiento, tenemos una sala para 140 personas, la misma que para Con 8 Basta y la gente que asistió dejó el listón muy alto. Reservad los días 7, 8 y 9 de septiembre para desarrollar una aplicación ganadora. Os podéis registrar en la siguiente url http://hackathonw8palma.eventbrite.com/

¡Que disfrutéis del verano!

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.

No sólo blog…

Magazines to readComo algunos de mis artículos los escribo en publicaciones online quería dejaros aquí una relación de lo que he publicado fuera del blog durante los últimos 12 meses, todo sobre Windows Phone:

Todos los artículos van acompañados del código de ejemplo.

Usar el micrófono de Windows Phone 7 desde Silverlight

Descarga el código de ejemplo aquí: WindowsPhoneMicrophoneSL.zip.
(Nueva versión corregida)

microphone
Hace unos meses escribí un artículo sobre cómo grabar un WAV con el micrófono del Windows Phone 7. En él explicaba cómo utilizar las clases de XNA desde Silverlight para poder leer la señal del micrófono y algún truco necesario para crear el archivo WAV.

Como sabréis ya ha salido la nueva versión Windows Phone 7.5 (AKA Mango) junto con el nuevo SDK 7.1.
Ahora ya podemos grabar la señal del micrófono desde Silverlight sin necesidad de usar XNA. Ya tenemos acceso a las clases de Silverlight 4, así podremos utilizar un AudioSink para poder grabar audio.

Para que funcione el micrófono en Silverlight necesitaremos habilitar el tag <Capability Name=“ID_CAP_CAMERA” />. He añadido una solicitud en UserVoice para que en próximas versiones no sea necesario y baste con el tag ID_CAP_MICROPHONE.

El código

La diferencia con el artículo anterior está en la clase MicrophoneWavRecorder, que ahora hereda de la clase AudioSink:

public sealed class MicrophoneWavRecorder : AudioSink
{
    private AudioFormat _format;
...

En el constructor solicitaremos acceso al dispositivo de captura y seleccionaremos el formato que más nos convenga utilizar, en nuestro caso 16 bits y algo mejor que 11Khz

Edit: por ahora no podemos seleccionar el formato, pidamos lo que pidamos el micrófono seguirá capturando a 16 bits Mono 16Khz, así que mejor no indicamos nada en ese punto.

public MicrophoneWavRecorder()
{
    if (!CaptureDeviceConfiguration.AllowedDeviceAccess)
        CaptureDeviceConfiguration.RequestDeviceAccess();

    AudioCaptureDevice device =
        CaptureDeviceConfiguration.GetDefaultAudioCaptureDevice();
    if (device == null)
    {
        throw new Exception("Can't get capture device!");
    }

    //Por ahora no podemos seleccionar el formato, esperemos que más adelante si...
//    _format = (from af in device.SupportedFormats
//              where af.BitsPerSample == 16 && af.SamplesPerSecond > 11025
//              select af).First();

//    device.DesiredFormat = _format;
//    device.AudioFrameSize = 100;

    this.CaptureSource = new CaptureSource();
    this.CaptureSource.AudioCaptureDevice = device;
}

Como no hemos podido asignar el formato de audio preferido tendremos que esperar que el dispositivo nos indique qué formato ha elegido él, para esto tenemos el método OnFormatChange con el que podremos capturar la información:

protected override void OnFormatChange(AudioFormat audioFormat)
{
    //capturamos el formato
    //en windows phone 7.1 es: mono 16 bit 16000 Hz
    _format = audioFormat;
}

Ahora ya sólo nos queda capturar el audio cada vez que se llena el buffer:

protected override void OnSamples(long sampleTime,
    long sampleDuration, byte[] sampleData)
{
    _bwOutput.Write(sampleData, 0, sampleData.Length);
    _rawDataLength += sampleData.Length;
}

Una de las ventajas que tenemos usando la clase AudioSink es que tenemos acceso al formato real y ya no tenemos que escribir los datos de la cabecera con valores fijos como hacíamos en el ejemplo anterior en el método WriteHeader, tendremos que ir con cuidado pues el formato de audio no lo sabemos en la primera escritura, pero lo tendremos cuando cerremos el audio:

if (_format != null)
{
    // Channel Numbers (Always 0x01=Mono, 0x02=Stereo)
    _bwOutput.Write((ushort)_format.Channels);

    // Sample Rate (Binary, in Hz)
    _bwOutput.Write((uint)this._format.SamplesPerSecond);

    // Bytes Per Second
    _bwOutput.Write((uint)(_format.BitsPerSample *
        _format.SamplesPerSecond * _format.Channels / 8));

    // Bytes Per Sample: 1=8 bit Mono,
    // 2=8 bit Stereo or 16 bit Mono, 4=16 bit Stereo
    _bwOutput.Write((ushort)(_format.BitsPerSample
        * _format.Channels / 8));

    // Bits Per Sample
    _bwOutput.Write((ushort)_format.BitsPerSample);
}

¿Qué voy a necesitar?

  1. Un dispositivo: aquí no os puedo ayudar mucho, en el emulador no funciona el micrófono, aunque podéis poner otra solicitud en UserVoice.
  2. El SDK 7.1 que puedes encontrar en 9 idiomas, incluido el castellano: Windows Phone SDK 7.1
  3. Descarga el código completo de ejemplo aquí: WindowsPhoneMicrophoneSL.zip

Noticias MSDN actualizada a Mango

Mango

He actualizado recientemente la aplicación de demostración que uso en mis tutoriales de WP7 para aprovechar unas pocas de las más de 500 novedades que trae Mango. Dentro de poco estará disponible en el Marketplace, aquí os adelanto los cambios que he hecho por ahora.

 

Actualización a 7.1

Lo primero que debemos hacer es actualizar el proyecto al nuevo SDK. Basta pulsar con el botón derecho sobre el proyecto y obtendremos la opción que necesitamos:

Una vez realizada la actualización ya podemos realizar los cambios necesarios en nuestra aplicación.

¿Adormecida o Deshidratada?

El nuevo modelo de ejecución de WP7.5 contempla una nueva posibilidad al volver a nuestra aplicación, puede que la aplicación no vuelva “deshidratada” (tombstoning) sino en un estado latente aún en memoria, por lo tanto no hará falta recuperar los datos desde el PhoneApplicationService.Current.State y podremos continuar la ejecución como si nada hubiera pasado.
Execution Model Diagram for Windows Phone 7.5

Para saber si la aplicación se debe recuperar o no basta con mirar la propiedad IsApplicationInstancePreserved del evento de activación. En el caso de la aplicación de ejemplo no hace falta mirar esta propiedad, pues ya mirábamos si el ViewModel era válido o no.

Compartir

Incluir funcionalidad para las redes sociales se complica un poco si lo queremos hacer usando las API de cada red, pues debemos pedir permiso, abrirnos una cuenta de desarrollador y gestionar la comunicación (login, datos, etc…) con la interfaz de cada red. Como ya os conté en un post anterior, Mango incorpora una novedad que nos facilitará mucho la vida: la tarea ShareLinkTask, con la que podremos en un par de líneas compartir cualquier información en todas las redes que tenga el usuario configuradas en el teléfono, en nuestro código se ve así:

new ShareLinkTask()
{
    LinkUri = new Uri( rss.Link),
    Message = HtmlStripper.HtmlToPlainText(rss.Description),
    Title = rss.Title
}.Show();

Texto rico rico

En la página principal tenemos un apartado “Acerca de…” que indica algunos datos sobre la aplicación. Para mostrar el texto, en la versión anterior estaba escrita dentro de etiquetas TextBlock en un StackPanel, en el que se iban colocando uno sobre el otro los bloques de texto.

Ahora tenemos una mejor opción para presentar texto, el control RichTextBox que nos permitirá formatear, añadir imágenes y representar hiperenlaces dentro del texto:

<controls:PanoramaItem Header="Acerca de..." >
    <StackPanel>
        <RichTextBox IsReadOnly="True" Background="Transparent">
            <Paragraph TextAlignment="Justify">Esta es una aplicación no oficial para la
            </Paragraph>
            <Paragraph TextAlignment="Center">
            <Hyperlink NavigateUri="http://msdn.microsoft.com/es-ES/" 
                       TargetName="_blank">http://msdn.microsoft.com/es-ES/
            </Hyperlink>
            </Paragraph>
            <Paragraph TextAlignment="Justify">Los datos mostrados en la aplicación son 
            </Paragraph>
            <Paragraph></Paragraph>
            <Paragraph TextAlignment="Justify">Desarrollada por
                    Juan Manuel Servera
                 en Marzo de 2011 como ejemplo del tutorial de aplicaciones de WP7.
            </Paragraph>
            <Paragraph TextAlignment="Center">
                <Hyperlink NavigateUri="https://jmservera.wordpress.com"
                           TargetName="_blank">https://jmservera.wordpress.com
                </Hyperlink>
            </Paragraph>
        </RichTextBox>
    </StackPanel>
</controls:PanoramaItem>

Para que los hiperenlaces funcionen es importante que el control esté en modo lectura (IsReadOnly=”True”) y que los enlaces tengan un TargetName (“_blank” en nuestro caso).

Podéis descargar el código fuente completo de la aplicación en Codeplex.