Mejorar el rendimiento del canvas HTML5 desde C#


IMG_4102
Desarrollar aplicaciones para la Tienda Windows en JavaScript tiene sus ventajas e inconvenientes. Aunque en la mayoría de los casos no lo vamos a notar, cuando tratamos grandes cantidades de datos y tenemos que realizar modificaciones sobre estas, lo más probable es que se penalice el rendimiento.

Estoy desarrollando la nueva versión de Fingerpaint y quiero añadir una herramienta de relleno de color.
No es complicado de hacer, hay bastantes algoritmos bien optimizados y el context tiene los métodos getImageData y putImageData que nos dan el array de datos para modificar el contenido del canvas a nivel de pixel:

width = context.canvas.width;
height = context.canvas.height;
imageData = context.getImageData(0, 0, width, height);
data = imageData.data;
for(var i=0;i<data.length;i++){
  //tratar los píxeles de data uno por uno
}

El problema de hacer esto en JavaScript es que el lenguaje no está optimizado para este tipo de operaciones e intentar hacer un relleno un poco inteligente (estilo cubo de pintura) va a tardar mucho más de lo que estamos acostumbrados con cualquier aplicación de dibujo moderna.

Componentes Windows Runtime (WinRT)

Por suerte, mi aplicación FingerPaint es para Windows 8 (en realidad 8.1) y puedo usar librerías desarrolladas en otros lenguajes, como C# o C++, que me permitirán realizar estas operaciones mucho más rápido.
Es muy fácil tener código en otros lenguajes dentro de una aplicación JavaScript de la tienda Windows, sólo tenemos que añadir una nueva librería de tipo Windows Runtime Component en el lenguaje que prefiramos. Con este tipo de proyectos estamos indicando que es una librería que puede utilizarse desde cualquier otro lenguaje.

Lo que estamos haciendo es crear una librería con WinMD, el mismo sistema de metadatos que las librerías de sistema en Windows Runtime. En esta imagen creamos un nuevo componente en C#:
Windows Runtime Component

A continuación tenéis el ejemplo de código. Está dividido en dos métodos:

public sealed class Fill
{
   private byte[] _data;

   public void BeginFill(int x, int y, int width, int height, 
       [ReadOnlyArray] byte[] bitmap, string hexColor, byte tolerance)
   {
     _data=bitmap;
     //
     //aquí irá mi código de flood fill que modificará los datos
     //dentro de _data, pero al ser ReadOnly no volverán a JavaScript
     //modificados.
     //
   }

   public void EndFill([WriteOnlyArray] byte[] target)
   {
     _data.CopyTo(target,0);
   }
}

Si os fijáis, la clase es sealed y los parámetros array están decorados con los atributos Read/WriteOnlyArray. Estas dos características son requisito indispensable para que nuestra clase y métodos se puedan utilizar desde cualquier otro lenguaje.

Añadiremos el proyecto que hemos creado en las referencias de nuestra aplicación JavaScript y desde el código JavaScript las llamadas se verán así, como si fueran llamadas a cualquier otra librería JavaScript, pero tú y yo sabemos que está hecho en C#:

width = context.canvas.width;
height = context.canvas.height;
imageData = context.getImageData(0, 0, width, height);
data = imageData.data;
//creamos un objeto Fill
var helper=new ImageHelpers.Fill();
//calculamos el fill
helper.beginFill(x,y,width,height,data,"#00ff00",50);
//escribimos el resultado en imageData
helper.endFill(data);
//escribimos el resultado en el canvas
context.putImageData(imageData, 0, 0);

Para poder modificar el canvas necesitamos que nuestro método externo modifique directamente el array obtenido con getImageData, pero cuando trabajamos con componentes de WinRT y queremos pasar un array sólo podemos hacerlo en uno de estos dos modos:

  • ReadOnlyArray: trabajamos sobre una copia del array y cualquier modificación no se traspasará entre los contextos, ni siquiera si devolvemos el array como valor de retorno
  • WriteOnlyArray: podemos escribir sobre el array original… pero este siempre viene vacío, no veremos la información original

Adicionalmente, canvas no nos permite introducir un array de datos dentro de imageData que no haya sido creado por el propio canvas, así que si utilizáramos un parámetro de retorno tendríamos que copiar uno por uno los bytes del array, con lo que perderíamos el rendimiento que ganamos por hacerlo en C#.
Entonces, y por eso el artículo de hoy, tenemos que realizar la operación en dos pasos: en el primero obtenemos los datos originales y en el segundo escribimos sobre la memoria del canvas desde C#. Estas operaciones son bastante rápidas y casi no penalizan el rendimiento (entre 1 y 3 milisegundos en un i5), así que nos queda bastante margen para modificar el contenido del canvas y mejorar considerablemente la velocidad del fill con nuestro método C#.

Depuración híbrida

Para poder depurar nuestro código C# dentro de una aplicación JavaScript tendremos que indicar en las propiedades del proyecto qué tipo de lenguaje queremos depurar, a alguien se le olvidó poner Managed+Script así que por ahora sólo podremos depurar en uno o en otro, si hubiéramos elegido C++ como lenguaje no tendríamos este problema, pero tendríamos otros :P.
Script or Managed debug

Happy finger painting!

Anuncios

Un Comentario

  1. Pingback: Consumir SOAP desde WinJS | Mouseless Me

Responder

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

Logo de WordPress.com

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

Imagen de Twitter

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

Foto de Facebook

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

Google+ photo

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

Conectando a %s