[UNIVERSAL] Win2D, Gráficos acelerados por hardware (1 de 2)

spaceship

En Septiembre, Microsoft liberó la primera versión de una librería gráfica para WinRT, llamada Win2D. Se trata de un wrapper sobre Direct2D, compatible con aplicaciones universales. Lo mejor de esto es que nos ofrece una forma muy sencilla de acceder a la potencia de DirectX para dibujar gráficos en dos dimensiones, sin tener que irnos a desarrollar código C++.

El desarrollo de Win2D está en progreso y el equipo va liberando versiones según terminan sprints, por lo que tenemos que tener en cuenta que puede haber fallos, bugs y que no está totalmente implementado Direct2D en este momento. No obstante empieza a ser una herramienta muy interesante y con la que podemos hacer cosas relativamente “resultonas” de forma muy sencilla.

Es importante destacar los tres principios de diseño sobre los que el equipo de Win2D trabaja:

  1. Todo lo que se pueda hacer con Win2D, debe poder hacerse con Direct2D. No se incluirán APis extras en Win2D. De esta forma simplemente se expone Direct2D a más desarrolladores.
  2. Crear una superficie de API que imite a Direct2D de forma que portar código sea más sencillo.
  3. Compartir el código y los progresos con la comunidad de desarrolladores.

Una vez que hemos visto un poco sobre qué es Win2D… ¿Para qué sirve? Podríamos pensar que una libraría 2D en pleno 2014 es algo con una utilidad limitada, pero nada más lejos de la realidad. Precisamente su punto fuerte es que solo es 2D, sencillo y antiguo 2D. Nos quitamos del medio la complejidad del 3D, el consumo de recursos del 3D y nos beneficiamos de una forma de dibujar gráficos bidiménsionales con aceleración por hardware. Podrías usar Win2D para un juego, pero también para hacer un increible fondo de pantalla animado y con aceleración para tu próxima aplicación. Incluso Win2D incluye algunos filtros, muy fáciles de implementar, para imágenes.

Vamos a comenzar por el principio.. Donde está el paquete de NuGet?

Where is my NuGet?

Podemos buscar el paquete de NuGet de Win2D hasta la saciedad y lo cierto es que no lo encontraremos. Actualmente el equipo de Win2D publica todo su código en un repositorio de GitHub, con lo que para usar esta librería tendremos que compilarnos nosotros mismos el paquete. Aunque tranquilos, porque es realmente sencillo de hacer.

Desde Visual Studio, en el panel “Team Explorer”,  vamos a la opción “Connect to team projects” (El icono en forma de enchufe en la parte superior). En la parte inferior encontraremos “Local Git Repositories” y entre otras opciones, una llamada “Clone”. Al presionarla nos pedirá la ruta del repositorio que queremos clonar. En la página del repositorio de GitHub de Win2D podemos encontrar la url para clonarlo, que es la siguiente:

https://github.com/Microsoft/Win2D.git

Simplemente introducimos esta URL en la primera caja de texto y seleccionamos el directorio local donde queramos guardar el repositorio local y presionamos el botón “Clone”:

image

Una vez terminado de clonar, tenemos que abrir una consola de símbolo de sistema de desarrollador (Developer command prompt), navegar al directorio donde hemos descargado el repositorio y ejecutar el archivo build.cmd.

build process

Tendremos que tener un poco de paciencia, porque el proceso tarda un poco en completarse. Al terminar es posible que nos indique que falta NuGet.exe y una línea de comandos a ejecutar para descargarlo. Lo descargamos, vamos al directorio buildnuget y ejecutamos el archivo build-nupkg.cmd y voila! en el directorio bin ya tendremos nuestros dos paquetes NuGet de Win2D.

image

Solo tenemos que irnos ya a las opciones de Visual Studio en el menú “Tools” y en opciones de NuGet agregar la ruta de los paquetes NuGet de Win2D como una nueva fuente de paquetes, de forma que podamos referenciarlos en nuestro proyecto.

Tenemos dos paquetes a nuestra disposición: Win2D y Win2D debug. El segundo permite que depuremos la propia librería Win2D. En este artículo uso la librería normal.

Superficie de dibujo

Una ves que hemos añadido el paquete NuGet a un proyecto universal, podemos empezar a usarlo. La gran baza de Win2D es la sencillez para integrarlo con XAML. Simplemente debemos añadir un nuevo namespace en la página donde queramos usar Win2D apuntando a Microsoft.Graphics.Canvas y añadir a la página el control CanvasControl contenido en ese namespace:

<Page
    x:Class=«Win2DSampleUniversal.MainPage»
    xmlns=«http://schemas.microsoft.com/winfx/2006/xaml/presentation»
    xmlns:x=«http://schemas.microsoft.com/winfx/2006/xaml»
    xmlns:local=«using:Win2DSampleUniversal»
    xmlns:d=«http://schemas.microsoft.com/expression/blend/2008»
    xmlns:mc=«http://schemas.openxmlformats.org/markup-compatibility/2006»
    xmlns:win2d=«using:Microsoft.Graphics.Canvas»
    mc:Ignorable=«d»>

    <Grid>
        <win2d:CanvasControl x:Name=«baseCanvas»/>
    </Grid>
</Page>

Ya tenemos una superficie sobre la que dibujar. Para poder mostrar gráficos, debemos usar una sesión de dibujo, representada por el objeto CanvasDrawingSession. Este objeto se obtiene de la propiedad DrawingSession de CanvasDrawEventArgs, en el evento Draw del CanvasControl, por lo que tendremos que manejarlo para poder dibujar.

Podríamos crear un código como el siguiente para el evento Draw:

private void baseCanvas_Draw(CanvasControl sender, CanvasDrawEventArgs args)
{
    using (var drawSession = args.DrawingSession)
    {
        drawSession.Clear(Colors.Blue);

        Vector2 center = new Vector2()
        {
            X = (float)sender.ActualWidth / 2.0f,
            Y = (float)sender.ActualHeight / 2.0f
        };
        float radius = 150;

        drawSession.FillCircle(center, radius, Colors.LightGreen);
    }
}

Como CanvasDrawingSession implementa IDisposable y además su ciclo de vida es el mismo que el de la ejecución del evento, una vez terminado el evento se pierde la sesión, usamos un bloque using para obtener la instancia de CanvasDrawEventArgs.

Una vez hecho esto, CanvasDrawingSession expone una serie de métodos que nos permiten dibujar diferentes formas como círculos, elipses, texto, rectángulos, líneas o imágenes. También nos permite borrar el contenido de la pantalla para empezar a dibujar de nuevo.

En el código anterior, primero borramos el contenido del canvas estableciendo el fondo azul, a continuación creamos una instancia de la clase Vector2 para definir el centro de un circulo, una variable float para definir el radio y usamos el método FillCircle para dibujarlo, indicando en el último parámetro el color del que queremos rellenarlo. El resultado es el siguiente:

image

Nada espectacular hasta ahora. Además el contenido es estático, el evento Draw solo se invoca una vez al comienzo de la aplicación o si redimensionamos la ventana que contiene la aplicación WinRT. En realidad lo que ocurre es que el método Draw se ejecuta cuando se invalida la superficie del control CanvasControl. Podríamos crear un DispatcherTimer que simplemente la invalidase cada X milisegundos. Si hacemos los cálculos, veremos que para conseguir una tasa de refresco de 60 cuadros por segundo, debemos ejecutar el DispatcherTimer cada 33 milisegundos, más o menos. Por lo que, simplemente en el evento Tick del DispatcherTimer, vamos a invalidar el redibujado. Primero inicializamos el timer en el método OnNavigatedTo de nuestra página:

protected override void OnNavigatedTo(Windows.UI.Xaml.Navigation.NavigationEventArgs e)
{
    base.OnNavigatedTo(e);

    this.random = new Random();
    this.timer = new DispatcherTimer();
    this.timer.Interval = new TimeSpan(0, 0, 0, 0, 30);
    this.timer.Tick += timer_Tick;
    this.timer.Start();
}

En el evento Tick, debemos parar el timer, para evitar que cualquier operación que se alargue más de 30 milisegundos haga que se ejecute varias veces sin llegar a dibujar. A continuación llamamos al método Invalidate de nuestro CanvasControl y se volverá a lanzar el evento Draw:

private void timer_Tick(object sender, object e)
{
    this.timer.Stop();
    baseCanvas.Invalidate();
}

Pero… ¿No vuelves a arrancar el DispatcherTimer al terminar el evento Tick? En realidad… lo deberíamos arrancar de nuevo cuando haya terminado de dibujar, y ya no tengamos nada más que hacer. Esto es tan solo una forma de hacerlo y cada uno debe experimentar con la cadencia de tiempo, con la forma de parar y arrancar el timer… para adaptarlo a lo que necesitemos en cada desarrollo.

Bien, ya podemos invalidar y redibujar cada XX milisegundos pero… ¿De qué nos vale? En el método Draw solo dibujamos un círculo una y otra vez. Pero no se trata de un objeto al que podamos acceder desde fuera… es una simple llamada al método FillCircle. Vamos a complicar un poco más el ejemplo. Como me gusta mucho la ciencia ficción, sobre todo la que trata sobre el espacio… vamos a hacer una vista del espacio con planetas. En primer lugar, ¿Como podemos definir un planeta? Quizás podríamos necesitar saber su posición con respecto a nuestro punto de vista, el radio central para definir su tamaño, el radio de sus anillos (si tiene claro…) El color de su cuerpo y el color de sus anillos. Incluso su aceleración, pues normalmente se trata de cuerpos celestes en movimiento. Así que podríamos desgranarlo en una clase más o menos como esta:

public class Planet
{
    public Vector2 Position { get; set; }

    public float BodyRadius { get; set; }

    public float RingsRadius { get; set; }

    public CanvasRadialGradientBrush BodyColor { get; set; }

    public Color RingColor { get; set; }

    public float Acceleration { get; set; }
}

Podría funcionar… Como podemos ver, el color del cuerpo del planeta se define con una propiedad del tipo CanvasRadialGradientBrush. Este nuevo tipo de brocha, nos permite crear un degradado radial, en ves de uno lineal. Los que hayáis trabajado en WPF, abrazaros y llorad de alegría. Estoy con vosotros.

Para crear un degradado CanvasRadialGradientBrush, al crear la instancia necesitaremos pasarle cierta información:

  • El CanvasControl donde vamos a pintarlo.
  • El color inicial (central)
  • El color final (borde exterior)

Además, una vez hecho esto, necesitaremos indicarle tres propiedades más: Center, RadiusX y RadiusY:

var gradient = new CanvasRadialGradientBrush(baseCanvas, Colors.Yellow, Colors.Orange);
gradient.Center = new Vector2() { X = 10, Y = 10 };
gradient.RadiusX = 40.0f;
gradient.RadiusY = 40.0f;

Es muy importante que el centro sea el mismo que el del objeto donde lo queremos usar. Las coordenadas que indicamos aquí, no son relativas al objeto donde lo apliquemos, son relativas a la pantalla. Es muy importante esto, o podemos darnos cabezazos contra la mesa pensando en porqué no se pinta el degradado correctamente.

Bien, ahora que sabemos como crear el degradado, vamos a crear un método llamado CreatePlanet, al que le pasemos ciertos valores y cree una instancia de un objeto de tipo Planet y lo guarde en una lista:

private void CreatePlanet(CanvasRadialGradientBrush bodyColor, float bodyRadius, float acceleration)
{
    var planet = new Planet();
    Vector2 center = new Vector2()
    {
        X = (float)this.random.Next(0, (int)baseCanvas.ActualWidth),
        Y = (float)this.random.Next(0, (int)baseCanvas.ActualHeight)
    };

    planet.Position = center;
    planet.BodyRadius = bodyRadius;
    planet.BodyColor = bodyColor;
    planet.BodyColor.Center = planet.Position;
    planet.BodyColor.RadiusX = planet.BodyRadius;
    planet.BodyColor.RadiusY = planet.BodyRadius;
    planet.RingColor = planet.BodyColor.Stops.Last().Color;
    planet.RingsRadius = this.random.Next((int)planet.BodyRadius, (int)planet.BodyRadius * 2);
    planet.Acceleration = acceleration;
    this.planets.Add(planet);
}

Este código simplemente rellena las propiedades que hemos indicado anteriormente del planeta. Como podemos ver, a la propiedad BodyColor (CanvasRadialGradientBrush) le pasamos los mismos valores que a la clase Planet para el centro y el radio X e Y.

Pero Yeray, esto es una clase normal y corriente, no es un objeto especial de DirectX super chulo y emocionante… Tranquilos a todo llegaremos. Para lo que nos va servir esta clase es para definir que hay que pintar, de una forma sencilla. Por ejemplo, podemos crear 125 instancias de la clase Planet, muy pequeñas y con un degradado radial de blanco a transparente, con este código:

for (int i = 0; i < 125; i++)
{
    CreatePlanet(new CanvasRadialGradientBrush(baseCanvas, Colors.White, Colors.Transparent),
                 this.random.Next(5, 10), (float)this.random.NextDouble());
}

El radio será de entre 5 y 10 píxeles, y la aceleración un valor aleatorio entre 0 y 1. En nuestro evento Draw, podemos escribir ahora código que recorra la lista de planetas y usar el método FillCircle para dibujar cada uno:

private void baseCanvas_Draw(CanvasControl sender, CanvasDrawEventArgs args)
{
    using (var drawSession = args.DrawingSession)
    {
        drawSession.Clear(Colors.Black);

        foreach (Planet item in this.planets)
        {
            drawSession.FillCircle(item.Position, item.BodyRadius, item.BodyColor);
            drawSession.DrawCircle(item.Position, item.RingsRadius, item.RingColor);
        }
    }

    if (this.timer != null)
        this.timer.Start();
}

Usamos el método FillCiircle para crear el círculo y rellenarlo con el color de cuerpo (El degradado radial que creamos anteriormente) e indicamos la posición y el radio. A continuación usamos el método DrawCircle, para dibujar exactamente igual un círculo, pero en esta ocasión sin relleno, solo dibujando el borde con el color de anillo y la misma posición. Si ejecutamos este código en el simulador de Windows Store, obtendremos algo parecido a esto:

image

Simplemente con un bucle ya hemos dibujado algo parecido a un campo de estrellas… con algo de imaginación al menos. Ahora, vamos a aprovecharnos del DispacherTimer para agregar algo de lógica relacionada con la propiedad Acceleration que hemos incluido en la clase Planet. A ver que os parece:

private void timer_Tick(object sender, object e)
{
    this.timer.Stop();

    foreach (Planet planet in this.planets)
    {
        planet.Position = new Vector2()
        {
            X = planet.Position.X,
            Y = planet.Position.Y < baseCanvas.ActualHeight ? planet.Position.Y + planet.Acceleration : 60
        };
        planet.BodyColor.Center = planet.Position;
    }

    baseCanvas.Invalidate();
}

Con el código anterior, antes de invalidar el CanvasControl, recorremos la lista de planetas y creamos una nueva instancia de Vector2, dejamos la posición X en el mismo punto en el que se encuentra. Con la posición Y, comprobamos si es menor que el area vertical de la pantalla. Si lo es, a la posición Y actual le añadimos la aceleración de la instancia de planeta. Si es mayor, reseteamos la posición a un valor negativo, fuera de la pantalla para que continue avanzando.

Con esto, conseguimos un movimiento vertical descendente de los círculos. Pero además como no todos los planetas tienen la misma aceleración, obtenemos el efecto de profundidad.

Ahora, con el código que hemos creado, podemos seguir creando más planetas, más grandes, con diferentes colores y con más aceleración de forma que obtengamos una mayor sensación de profundidad:

private async Task CreateObjects()
{
    for (int i = 0; i < 125; i++)
    {
        CreatePlanet(new CanvasRadialGradientBrush(baseCanvas, Colors.White, Colors.Transparent),
                     this.random.Next(5, 10), (float)this.random.NextDouble());
    }

    for (int i = 0; i < 7; i++)
    {
        CreatePlanet(new CanvasRadialGradientBrush(baseCanvas, Colors.DarkGoldenrod, Colors.Maroon),
                     this.random.Next(20, 30), (float)this.random.Next(1, 2));
    }

    for (int i = 0; i < 4; i++)
    {
        CreatePlanet(new CanvasRadialGradientBrush(baseCanvas, Colors.Goldenrod, Colors.DarkOrange),
                     this.random.Next(40, 50), (float)this.random.Next(3, 4));
    }

    for (int i = 0; i < 3; i++)
    {
        CreatePlanet(new CanvasRadialGradientBrush(baseCanvas, Colors.Yellow, Colors.Orange),
                     this.random.Next(65, 75), (float)this.random.Next(6, 7));
    }
}

Simplemente añadiendo estos nuevos planetas, nuestro código los dibujará, de una forma parecida a la siguiente:

image

Esto ya parece otra cosa… además, como hemos añadido diferentes aceleraciones en cada capa, desde más lenta a más rápida, obtenemos un mejor efecto de profundidad. Lo se, no es que sea precisamente lo último en tecnología gráfica, pero ilustra lo que os quiero contar jeje.

Ahora que tenemos un campo de estrellas en su lugar, nos falta ¡una nave espacial! y cual que mejor que un X-Wing.

Afortunadamente Win2D incluye métodos y objetos para trabajar con imágenes de forma muy sencilla. Uno de esos objetos es el CanvasBitmap, que nos permite cargar una imagen almacenada en un archivo en el proyecto:

var img = await CanvasBitmap.LoadAsync(baseCanvas, @»AssetsSpaceship.png»);

Usando el método estático LoadAsync y pasándo como parámetro nuestro CanvasControl y la ruta relativa de la imagen en el proyecto, tenemos creado un objeto CanvasBitmap.

Una vez creado el objeto CanvasBitmap, en nuestro método Draw lo podemos mostrar usando el método DrawImage con tres parámetros:

  • el CanvasBitmap a dibujar
  • una instancia de Rect (Windows.Foundation.Rect) para definir la posición (X, Y) y el tamaño de destino.
  • una instancia de Rect  para definir la posición (X, Y) y el tamaño dentro de la imagen original, que queremos pintar.

El que el segundo Rect nos permita definir un fragmento de la imagen, es especialmente útil para trabajar con sprites.  Pero no todo va a ser tan facil. Al igual que con la clase Planet, vamos a definir una clase Spaceship que identifque a nuestra nave espacial. Esta vez es un poco más sencilla: una posición, un desplazamiento negativo y una imagen a dibujar:

public class Spaceship
{
    public Vector2 Position { get; set; }

    public Vector2 NegativeDisplacement { get; set; }

    public CanvasBitmap Image { get; set; }
}

La posición y la imagen, están claras. Pero, ¿Desplazamiento negativo? Bueno, ha sido una idea por darle un poco más de lógica a la demo. ¿Y si al mover la nave en cualquier dirección, aplicamos la misma cantidad de movimiento a las estrellas y planetas, pero en la dirección contraria? Le damos un poco más de interactividad a la lógica.

Primero lo primero, en el mismo método que creaba los planetas, vamos a crear la instancia de la clase Spaceship que vamos a usar:

this.spaceship = new Spaceship();
this.spaceship.Image = await CanvasBitmap.LoadAsync(baseCanvas, @»AssetsSpaceship.png»);
this.spaceship.Position = new Vector2()
{
    X = (float)(baseCanvas.ActualWidth / 2) 60,
    Y = (float)(baseCanvas.ActualHeight / 1.5) 55
};

Simplemente le asignamos el CanvasBitmap y posicionamos la imagen en el centro de la pantalla. Para ello, calculamos el centro y le restamos la mitad del tamaño con el que queremos dibujar la nave.

En nuestro evento Draw, tras dibujar los planetas, vamos a añadir código para dibujar la nave:

if (this.spaceship != null && this.spaceship.Image != null)
{
    drawSession.DrawImage(this.spaceship.Image,
                          new Rect(this.spaceship.Position.X, this.spaceship.Position.Y, 120, 110),
                          new Rect(0, 0, 1952, 1857));
}

Como dijimos anteriormente, el primer Rect indica el destino y el segundo indica el pedazo de la imagen que queremos mostrar. En este caso toda la imagen, que como veréis no me he molestado en disminuir desde una resolución mastodóntica… pensando en hacer algunos juegos de ampliar y reducir la imagen, para dar efecto de elevación… Pero eso para otro tutorial.

image

Ya solo nos queda una cosa: Poder interactuar con la nave moviéndola al pulsar sobre ella. Para ello, vamos a definir la propiedad ManipulationMode del CanvasControl y manejar los eventos: ManipulationStarted, ManipulationDelta y ManipulationCompleted:

<win2d:CanvasControl x:Name=«baseCanvas» VerticalAlignment=«Stretch» HorizontalAlignment=«Stretch»
                     Draw=«baseCanvas_Draw»
                     ManipulationMode=«All»
                     ManipulationStarted=«baseCanvas_ManipulationStarted»
                     ManipulationCompleted=«baseCanvas_ManipulationCompleted»
                     ManipulationDelta=«baseCanvas_ManipulationDelta»
                     Margin=«-50»/>

Como solo queremos manipular la “nave” cuando el usuario pulse sobre ella y la arrastre, en el evento ManipulationStarted, comprobamos si el punto sobre el que empieza la manipulación está contenido en el area de la imagen, definida por la posición + el tamaño. Si está contenida, señalaremos una variable boolean indicando que efectivamente estamos manipulando la nave:

void baseCanvas_ManipulationStarted(object sender, ManipulationStartedRoutedEventArgs e)
{
    if ((e.Position.X >= this.spaceship.Position.X && e.Position.X <= this.spaceship.Position.X + 120) &&
        (e.Position.Y >= this.spaceship.Position.Y && e.Position.Y <= this.spaceship.Position.Y + 110))
    {
        this.movingSpaceship = true;
    }
}

Simplemente comprobamos si la posición X, Y de ManipulationStartedRoutedEventArgs, está dentro del area de la nave. Una vez hecho esto, es en el evento ManipulationDelta donde recibiremos la información mientras el usuario arrastra la imagen, y donde modificaremos su posición:

 

void baseCanvas_ManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
{
    if (this.movingSpaceship)
    {
        this.spaceship.Position = new Vector2()
        {
            X = (float)(this.spaceship.Position.X + e.Delta.Translation.X),
            Y = (float)(this.spaceship.Position.Y + e.Delta.Translation.Y)
        };

        this.spaceship.NegativeDisplacement = new Vector2()
        {
            X = (float)e.Delta.Translation.X / 1,
            Y = (float)e.Delta.Translation.Y / 1
        };
    }
}

 

Primero comprobamos si estamos manipulando la nave. A continuación, usamos la propiedad Delta de ManipulationDeltaRoutedEventArgs para obtener la cantidad de traslación realizada desde la última vez que se lanzó el evento. Normalmente el valor es bastante pequeño y relativo al punto donde hayamos tocado. Si desplazamos el puntero/dedo hacia abajo o hacia la derecha, tendremos un valor positivo. Si lo desplazamos hacia arriba o izquierda, un valor negativo. A continuación, cogemos ese mismo valor y obtenemos su inverso (si es 3.2, obtenemos –3.2) y lo guardamos en el Vector2 NegativeDisplacement. Un poco más adelante lo vamos a usar en nuestra lógica.

Por último, el evento ManipulationCompleted nos informa de que el usuario ha dejado de actuar sobre el objeto. En ese momento, podemos establecer la variable boolean a false de nuevo, ya no existe manipulación y reseteamos el NegativeDisplacement a 0, pues ya no hay movimiento:

void baseCanvas_ManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e)
{
    this.movingSpaceship = false;
    this.spaceship.NegativeDisplacement = new Vector2() { X = 0, Y = 0 };
}

Como podemos ver, en ninguno de estos eventos invalidamos el CanvasControl. No lo necesitamos, nuestro DispatcherTimer sigue activo y lo que vamos a hacer es añadir la lógica que tenga en cuenta el NegativeDisplacement a la hora de calcular el movimiento de los objetos:

 

private void timer_Tick(object sender, object e)
{
    this.timer.Stop();

    foreach (Planet planet in this.planets)
    {
        planet.Position = new Vector2()
        {
            X = planet.Position.X < baseCanvas.ActualWidth ?
                planet.Position.X + this.spaceship.NegativeDisplacement.X :
                (float)this.random.Next(0, (int)baseCanvas.ActualWidth),

            Y = planet.Position.Y < baseCanvas.ActualHeight ?
                planet.Position.Y + planet.Acceleration + this.spaceship.NegativeDisplacement.Y :
                60
        };

        planet.BodyColor.Center = planet.Position;
    }

    baseCanvas.Invalidate();
}

 

En primer lugar, tanto en el valor X como Y, comprobamos si estamos dentro de la pantalla. Si estamos dentro, en el eje X le sumamos a la posición actual el valor de X de NegativeDisplacement de la nave. En el eje Y a la posición ya le sumamos el valor de aceleración del propio planeta, para crear la sensación de movimiento. Ahora además le añadimos el valor de Y de NegativeDisplacement. Si estamos fuera de la pantalla o a punto de salir de ella, en el eje X volvemos a calcular un valor aleatorio de X para repintar el planeta en otra parte de la pantalla. En el eje Y simplemente lo pintamos al principio de la pantalla, unos píxeles fuera, para que al animarlo vaya entrando en la misma.

Como podemos ver, no hacemos nada con la nave. En el evento ManipulationDelta ya hemos actualizado el Vector2 Position, con lo que el propio evento Draw la dibujará en la posición adecuada.

Y con esto, si ejecutamos tanto en Windows Store como Phone, podremos mover la nave en todas direcciones y veremos como las estrellas se desplazan en la dirección inversa.

Lo único que nos queda por ver es… como se integra con el resto del XAML. La verdad es que no es ningún misterio, tan solo tenemos que poner el XAML que queramos mostrar, a continuación del CanvasControl y funcionará automáticamente. Por ejemplo, por completar la imitación de un juego de naves, yo he añadido un TextBlock y una ProgressBar que muestran la “energía” restante de la nave:

<Grid>
    <win2d:CanvasControl x:Name=«baseCanvas» VerticalAlignment=«Stretch» HorizontalAlignment=«Stretch»
                         Draw=«baseCanvas_Draw»
                         ManipulationMode=«All»
                         ManipulationStarted=«baseCanvas_ManipulationStarted»
                         ManipulationCompleted=«baseCanvas_ManipulationCompleted»
                         ManipulationDelta=«baseCanvas_ManipulationDelta»
                         Margin=«-50»/>

    <StackPanel VerticalAlignment=«Top» HorizontalAlignment=«Stretch» IsHitTestVisible=«False»>
        <TextBlock Text=«Energy:» FontSize=«24» FontWeight=«Bold» Foreground=«Cyan» Margin=«24,24,0,0»/>
        <ProgressBar x:Name=«EnergyBar» Minimum=«0» Maximum=«100» Value=«100» Background=«Transparent»
                     Margin=«24,12,24,0» Height=«12»>
            <ProgressBar.Foreground>
                <LinearGradientBrush StartPoint=«0,0» EndPoint=«1,0»>
                    <GradientStop Offset=«0» Color=«Red»/>
                    <GradientStop Offset=«.5» Color=«Yellow»/>
                    <GradientStop Offset=«1» Color=«Green»/>
                </LinearGradientBrush>
            </ProgressBar.Foreground>
        </ProgressBar>
    </StackPanel>
</Grid>

Simplemente colocamos el XAML como con cualquier otra página. El resultado final de todo el trabajo que hemos hecho, una app universal, compartiendo todo el código, tanto para Windows como para Windows Phone:

image

Y llegamos al final! Creo que hemos dado un buen repaso por encima de las capacidades básicas de Win2D, como conseguirlo y como poder empezar a usarlo. Quedan más cosas que puede hacer, como aplicar filtros a imágenes, pero eso se va a quedar para el siguiente artículo.

Como siempre, aquí podéis descargaros el ejemplo usado en este artículo. Espero que lo disfrutéis tanto como yo lo he hecho escribiéndolo.

Un saludo y Happy Coding!

 

Un comentario sobre “[UNIVERSAL] Win2D, Gráficos acelerados por hardware (1 de 2)”

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *