HTML5 Apis – Crea tu propio Instagram

Sigamos con la serie de posts sobre las APIs de HTML5. Ahora le toca al canvas, uno de los elementos más revolucionarios de HTML5. Yo siempre digo que si <video /> hirío a Flash, entonces <canvas /> lo mata definitivamente.

Que es el canvas? Pues dicho rápido y mal: Un nuevo elemento de HTML, que nos permite tener una superficie de dibujo. El canvas por si mismo no tiene una API asociada, en su lugar se obtiene un contexto de dibiujo sobre el canvas. Es dicho contexto el que nos proporciona una API para interaccionar con el canvas.

En la actualidad hay dos especificaciones de contextos distintas:

  • 2d: Para operaciones de 2D. Contiene una API muy sencilla para por un lado dibujar en el canvas (líneas, círculos, cuadrados, etc) y por otra acceder directamente al contenido binario del canvas.
  • webgl: Para operaciones 3D. Ofrece una API basada en OpenGL ES 2.0.

Actualmente todos los navegadores soportan el contexto 2d, y la mayoría soportan webgl. La excepción es IE y la razón principal (al margen de ciertas vulnerabilidades descubiertas en webgl) es que no es un estándard W3C.

No voy a explicar en este post como dibujar en el canvas, hay multitud de tutoriales para ello. En su lugar vamos a ver como combinar el canvas, File Api, un poco de drag and drop y acceso al contenido binario del canvas para crearnos nuestro propio Instagram 🙂

Paso 1 – Declaración del <canvas />

Bueno… eso no tiene apenas ningún secreto, basta con añadir un tag <canvas> y darle un tamaño:

<div>

    <canvas height=»400″ width=»600″ id=»mc» styleborder: 1px solid black»></canvas> 

</div>

Paso 2 – Habilitar drag and drop

Ahora lo que queremos es que el usuario pueda hacer drop de un fichero local sobre el canvas y que nosotros lo podamos procesar. Todavía no hemos visto la API nativa de drag and drop que incorpora HTML5 pero vamos a ver lo básico ahora para habilitarla. Debemos seguir dos pasos para que un elemento sea una zona donde se pueda hacer drop:

  1. Asociarnos a su evento dragOver y allí hacer un preventDefault. Eso es porque por defecto todos los objetos del DOM heredan una implementación de dicho evento que significa “no se puede hacer drop aquí”. Al rededinir dicho evento quitamos este comportamiento por defecto y nuestro elemento del DOM pasa a ser un zona donde se puede hacer drop. El evento dragOver se dispara cuando estamos arrastrando algo y pasamos por encima del elemento.
  2. Asociarnos a su evento drop y allí recoger los datos que se hayan arrastrado. En el caso que nos ocupa (arrastre de ficheros locales hacia un elemento del DOM), el evento de drop tiene una propiedad dataTransfer que tiene una propiedad files que es una FileList (de File API), con la información de los ficheros que se hayan arrastrado.

Por lo tanto añadamos el mínimo código script para soportar el drag and drop:

<script type=»text/javascript»>

    var canvas = document.getElementById(«mc»);

    canvas.addEventListener(«dragover», handleDragOver, false);

    canvas.addEventListener(«drop», handleDrop, false);

    function handleDragOver(e) {

        e.preventDefault();

    }

 

    function handleDrop(e) {

        var files = e.dataTransfer.files;

        if (files.length > 0) {

            var file = files[0];

            procesarFichero(file);

        }

 

        e.preventDefault();

    }

 

    function procesarFichero(file) {

        alert(«has arrastrado « + file.name + «->» + file.type);

    }

</script>

Con esto ya tenemos el drag and drop habilitado para el canvas y podemos arrastrar y soltar en él, ficheros locales!

Paso 3 – Leer la imagen y colocarla en el canvas

De todas las funciones que nos ofrece el contexto 2d para el canvas, hay una que nos interesa ahora mismo que es drawImage. Esta función nos permite recoger el contenido de una imagen ya existente y colocarlo en el canvas. Por lo tanto antes de usar drawImage necesitamos tener una imagen cargada. Ya vimos en el post dedicado a File API como leer un objeto File y asignarlo a una imagen: con el método ReadAsDataURL. Bien, pues lo hacemos:

function procesarFichero(file) {

    var fr = new FileReader();

    fr.addEventListener(«loadend», function(e) {

        var datauri = e.target.result;

        loadCanvas(datauri);

    }, false);

 

    fr.readAsDataURL(file);

}

function loadCanvas(datauri) {

    alert(datauri);

}

Ahora tan solo nos falta colocar la imagen en el canvas. Para ello, primero la tenemos que colocar en un objeto Imagen y luego sí, ya en el canvas:

function loadCanvas(datauri) {

    var img = new Image();

    img.addEventListener(«load», function(e) {

        var ctx = canvas.getContext(«2d»);

        ctx.drawImage(img, 0, 0);

    }, false);

    img.src = datauri;

}

A destacar de este código anterior:

  1. El uso de getContext(«2d») para obtener el contexto 2d. Ojo, que la cadena que se pasa a getContext es case-sensitive!
  2. Creamos una imagen y le asignamos como src el resultado de readAsDataURL.
  3. Usamos drawImage para dibujar la imagen en el canvas. Importante que esto lo tenemos que hacer dentro del evento load de la imagen, para asegurarnos que esta estará cargada.

Paso 4: Crear un filtro

Vamos a crear un filtro para manipular nuestra imagen. En este ejemplo será muy sencillo: un botón que invertirá los colores de la imagen.

Para implementar este filtro debemos acceder al contenido binario del canvas. Dicho contenido es muy sencillo: cada pixel del canvas ocupa 4 bytes (rojo, verde, azul y canal alfa), y está guardado por filas (es decir, primero todos los pixels de la primera fila de izquierda a derecha y así sucesivamente).

El método getImageData del contexto, nos devuelve dicho array rellenado con el contenido actual del canvas. A dicho método se le pasa el rectángulo que queremos obtener (punto top-left, ancho y alto) y nos devolverá el array correspondiente. Cada píxel ocupará, insisto, 4 posiciones de dicho array (el array es de bytes).

Así me creo mi función de filtro, que simplemente invertirá los valores de r,g,b dejando el canal alfa tal cual estaba:

function applyFilter() {

    var ctx = canvas.getContext(«2d»);

    var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

    for (var idx = 0; idx < imageData.data.length; idx+=4) {

        var r = imageData.data[idx];

        var g = imageData.data[idx + 1];

        var b = imageData.data[idx + 2];

 

        imageData.data[idx] = 255 – r;

        imageData.data[idx + 1] = 255 – g;

        imageData.data[idx + 2] = 255 – b;

    }

    ctx.putImageData(imageData, 0, 0);

}

Ahora tan solo me falta añadir un botón a la página y enlazar el click del botón con el método applyFilter.

Paso 5: Enviar los datos del canvas al servidor

Una vez hemos modificado el canvas nos puede interesar mandar la imagen que hay en el canvas hacia el servidor. Para ello no hay ninguna función específica, así que veremos como podemos hacerlo.

Hay un método toBlob en el propio canvas que nos devuelve un Blob (es casi equivalente al File de File API que conocemos, de hecho File deriva de Blob), pero es muy nuevo y no está apenas implementado en ningún navegador, así que lo descartamos. Dicho método sería la manera más directa, ya que luego con un FormData podemos mandar directamente este Blob (como vimos en el post de uploads asíncronos). Pero como digo, queda descartado.

Pero bueno, tampoco es ningún drama. La solución pasa por crearnos el Blob a mano y rellenarlo con los datos del canvas… Veamos:

function uploadCanvas() {

    var ctx = canvas.getContext(«2d»);

    var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

    var xhr = new XMLHttpRequest();

 

    var arrayBuffer = new ArrayBuffer(imageData.data.length);

    var view = new Uint8Array(arrayBuffer);

    for (var idx = 0; idx < imageData.data.length; idx++) {

        view[idx] = imageData.data[idx];

    }

    var blob = new Blob([view]);

    var formData = new FormData();

    formData.append(«canvas», blob);

    formData.append(«w», canvas.width.toString());

    formData.append(«h», canvas.height.toString());

    xhr.open(«POST», «@Url.Action(«Index», «Instagram»)«, true);

    xhr.send(formData);

}

Para crear un Blob debemos crear antes un ArrayBuffer con los datos de dicho Blob, y para acceder (y rellenar) un ArrayBuffer debemos hacerlo a través de un Uint8Array. De ahí que el código es un poco liadillo.

Luego, además de los datos binarios del canvas, mandamos el ancho y el alto, para tenerlos en servidor.

Ahora nos falta la parte de servidor:

[HttpPost]

public ActionResult Index(HttpPostedFileBase canvas, int w, int h)

{

    var stream = canvas.InputStream;

    var bmp = new Bitmap(w, h);

    int r, g, b, a;

    for (var row = 0; row < h; row++)

    {

        for (var col = 0; col < w; col++)

        {

            r = stream.ReadByte();

            g = stream.ReadByte();

            b = stream.ReadByte();

            a = stream.ReadByte();

            bmp.SetPixel(col, row, Color.FromArgb(a, r, g, b));

        }

    }

    bmp.Save(«d:\filename.png», ImageFormat.Png);

    return new HttpStatusCodeResult(200);

}

¿Sencillo no? Creamos un bitmap, decodificamos los datos binarios del canvas y “pintamos” el Bitmap. Luego lo guardamos y listo 😉

Paso 6: Guardar en el cliente

Para finalizar esta maravilla que estamos creando vamos a añadir un botón para guardar la imagen modificada en el cliente, sin necesidad de subirla al servidor.

Para ello nos vamos a aprovechar de un método existente en el canvas llamado toDataURL, que como supondrás nos convierte el contenido del canvas a una data URL:

function saveCanvas() {

    var datauri = canvas.toDataURL(«image/png»);

    window.open(datauri, «preview», «width=» + (canvas.width + 20) +«,height=» + (canvas.height + 20));

}

Esta función la tengo enlazada al evento click de un botón y lo que hace es simplemente mostrar la imagen en un popup y así el usuario puede hacer «guardar como» y listos.

Existe una API definida (FileWriter) para escribir ficheros desde javascript pero su soporte es hoy en día tan minoritario que no tiene sentido plantearse el usarla. Otra alternativa, pero también minoritaria (sólo funciona con Chrome) sería usar un tag <a /> dinámico con el atributo download y forzar el click con javascript.

Saludos!

3 comentarios sobre “HTML5 Apis – Crea tu propio Instagram”

  1. Me podrías dar una mano con tu ejemplo?
    1)-en el punto 5- la parte del servidor que tipo de archivo es? JS PHP y como llamas a este desde el JS gracias.
    2)-el blob la función uploadcanvas la insertas directamente después de takepicture en el JS ?
    3) tendrías algún ejemplo con el código completo ?

    1. Buenas!
      1. Que el servidor sea PHP es irrelevante: la forma de llamarlo desde JS dependerá de como esté implementado el PHP. No puedo ayudarte más porque no sé mucho de PHP, pero al final todo es HTTP… Si tu función PHP acepta un POST con un fichero (enctype multipart/form-data) y dos campos llamados w y h te funcionará el código JS tal cual está en el post.

      2. La función uploadcanvas se llama como respuesta al click del botón de subir o del evento que desees. Dentro del fichero JS puedes meterla donde desees.

      3. No, lo siento. Lo perdí 🙁

      Gracias!

Deja un comentario

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