DÍA 18 / 2014

Generando Gifs con HTML5 Canvas y Javascript

Gif es uno de los formatos más famosos que podemos encontrar, este tuvo su repunte y éxito en la era de GeoCities y hoy en día en varias formas, colores y sabores en varias redes sociales o agregadores de contenido.
Es posible poder generar, con HTML5 y un par de librerías en Javascript, un output Gif a partir de un contexto canvas.


Para este tutorial utilizaremos dos librerías de dos personas increíbles que se dedicaron a portar la librería AS3GIF y AnimatedGifEncoder escrita en Java.

El Demo

Primero que nada y antes de dar la introducción a la librerías, vamos a construir un pequeño ejemplo. Generaremos una pequeñísima app para generar textos y animarlos con Canvas y Javascript. Clic en la imagen para ver el demo y mira el código de fuente.
Demo 1

Generando Gifs con HTML5 Canvas y Javascript 01

Rápidamente, analizemos el código para ver qué es lo que estaremos integrando: Existe un input de texto el cuál utilizaremos para animar las letras y una vez terminada la animación, hacemos clear del intervalo que iniciamos al hacer clic en el botón "Comenzar".

function drawText(text) {
  context.font = "50px Arial Black";
  context.textAlign = 'center';
  context.fillText(text, x, y);
}

// full_text - input del usuario
// curr_text - texto que actualmente se muestra
function drawFrame() {
  if (curr_text.length == full_text_arr.length) {
    clearInterval(frame);
  } else {
    curr_text.push(full_text_arr[curr_text.length]);
    drawText(curr_text.join(''));
  }
}

function animate() {
  frame = setInterval(function(){drawFrame()}, 100);
}

Por cada iteración del intervalo que mantiene la variable frame, empujamos una letra de full_text a curr_text utilizando la longitud de curr_text para determinar qué letra es la que sigue. Esta es una manera fácil y rápida para iterar y transferir valores de un array a otro sin utilizar una variable incremental para determinar la siguiente adición.

De Canvas a Gif animado

Por razón de aprendizaje y con ánimos de no ser un clásico tutorial copia y pega, vamos a utilizar dos librerías: una escrita en Javascript y más apegado a los métodos que nos encontraremos en AS3GIF y una librería muy bonita llamada Gif.js, que escrita bajo jQuery, nos da código más limpio y una implementación más sencilla sobre nuestra aplicación.

Port de GifEncoder

El developer antimatter15 se encargó de portar el GifEncoder de AS3GIF a Javascript, el cuál permite transformar bitmap a un bitmap binario que soporta compresión LZW y utiliza el algoritmo NeuQuant para cuantizar de una imagen de 32 bits RGBA a un esquema de 8 bits intentando perder la mínima calidad posible y también utilizaremos encoding y decoding en base64.

Estos archivos los podrás encontrar dentro del repositorio de antimatter15:jsgif. Soportado en la mayoría de los navegadores: Chrome, IE10+, Safari y mobile 6+, Firefox 17+.

El autor nos indica el órden recomendado en que debemos de incluir las librerías que anteriormente comenté.

<script src="LZWEncoder.js" type="text/javascript"></script>
<script src="NeuQuant.js" type="text/javascript"></script>
<script src="GIFEncoder.js" type="text/javascript"></script>
<script src="b64.js" type="text/javascript"></script>

Una vez incluidas tendremos acceso al Gif Encoder. Mantengamos el código sencillo.

Para iniciar un encoder, llamamos.

var encoder = new GIFEncoder();

Tendremos acceso de diferentes métodos para establecer configuraciones dentro del encoder, las que utilizaremos para este ejemplo son las siguientes.

encoder.setRepeat(0); // 0 loop, 1+ repeticiones
encoder.setDelay(500); // delay entre frames
encoder.setSize(width, height);
Nota: .addFrame(context) puede recibir directamente el contexto de nuestro Canvas o bien, un ImageData pasando un segundo argumento como .addFrame(ImageData, true), sin olvidar establecer el tamaño de nuestro input/output con .setSize(w,h).

Y así podremos hablar con el encoder y darle instrucciones.

encoder.start(); // iniciar encoding
encoder.addFrame(context); // agregar frame
encoder.finish(); // terminar encoding
encoder.stream(); // obtener stream generado

Puedes ver todas las opciones y métodos disponibles directamente en el código de fuente de GIFEncoder.js, está bastante bien documentado y organizado, o bien, en la documentación del repositorio.

Ahora realizaremos una integración básica de la librería para generar nuestro primer Gif animado utilizando el ejemplo anterior.

function drawText(text) {
  context.font = "50px Arial Black";
  context.textAlign = 'center';
  context.fillText(text, x, y);
}

// full_text - input del usuario
// curr_text - texto que actualmente se muestra
function drawFrame() {
  if (curr_text.length == full_text_arr.length) {
    finish(); // animacion terminada
  } else {
    curr_text.push(full_text_arr[curr_text.length]);
    drawText(curr_text.join(''));
    encoder.addFrame(context);
  }
}

function animate() {
  // iniciamos encoder
    encoder.start();
    // opciones
      encoder.setRepeat(0);
      encoder.setDelay(100);
      encoder.setSize(canvas.width, canvas.height); // importante
   
  frame = setInterval(function(){drawFrame()}, 100);
}

function finish() {
  clearInterval(frame);
  encoder.finish();
  data_url = 'data:image/gif;base64,' + encode64(encoder.stream().getData());
  output_image.src = data_url;
}

El output generado es el siguiente, puedes hacer clic en la imagen para mirar el código funcionando y mirar el código de fuente.
Demo 2

output1

Nuestro Gif es generado y encodeado en base64 en nuestra función finish(). Tendremos acceso a getData() para así poderselo mostrarlo en <img src/> o bien, mandarlo a nuestro servidor, forzar descarga, etc.

Podrás notar algo de lentitud comparando el Demo 1 y el Demo 2. Generar Gifs en el aire no es algo fácil de procesar y le alentarás el rendering del navegador de tu usuario, para poder evitar esto será necesario hacer la integración de un Web Worker, el cuál nos ayudará a procesar el encoding del Gif en un proceso aislado y utilizando directamente el poder del procesador del cliente.

La integración cambia bastante utilizando jsgif de antimatter15, tendremos que hacer un par de tweaks al código para poder enviar y recibir datos binarios entre el cliente y el proceso aislado para luego ser procesado, así que esto nos abre la puerta a la integración que hizo jnordberg:gif.js para facilitarnos la vida, ya que esta librería cuenta con la integración dinámica de multiples Web Workers y opciones al estilo jQuery.

Gif.js

Esta librería utiliza las mismas librerías que necesitaríamos en la integración de antimatter15 pero wrappeada bajo un plugin escrito en CoffeeScript y utilizando jQuery para convertirlo en plugin.

La integración, encoding y generación de Gifs se vuelve una tarea bastante sencilla cómo puedes ver en el ejemplo de su landing page.

También soportado en la mayoría de los navegadores: Chrome, IE10+, Safari y mobile 6+, Firefox 17+.

// configuración básica
var gif = new GIF({
  workers: 2,
  quality: 10
});

// agregar image element
gif.addFrame(imageElement);

// o agregar canvas directamente
gif.addFrame(canvasElement, {delay: 200});

// o agregar el contexto
gif.addFrame(ctx, {copy: true});

gif.on('finished', function(blob) {
  window.open(URL.createObjectURL(blob)); // generación de gif
});

gif.render(); // inicio de rendering

Puedes ver todas las opciones que podemos pasarle al plugin directamente en el repositorio.

La integración de gif.js en nuestro ejemplo, se construiría de manera más sencilla, rápida y con Web Workers dinámicos ya construidos y listos para usar.

function drawText(text) {
  context.font = "50px Arial Black";
  context.textAlign = 'center';
  context.fillText(text, x, y);
}

// full_text - input del usuario
// curr_text - texto que actualmente se muestra
function drawFrame() {
  if (curr_text.length == full_text_arr.length) {
    finish(); // animacion terminada
  } else {
    curr_text.push(full_text_arr[curr_text.length]);
    drawText(curr_text.join(''));
    gif.addFrame(context, {copy: true, delay: 100}); // agregamos frame
  }
}

function animate() {
  // opciones del encoder
  gif = new GIF({
    workers: 2,
    quality: 10,
    width: canvas.width,
    height: canvas.height
  });
   
  frame = setInterval(function(){drawFrame()}, 100);
}

function finish() {
  clearInterval(frame);
  gif.on('finished', function(blob) {
    output_image.src = URL.createObjectURL(blob); // gif animado
  });
  gif.render();
}

Y nos genera prácticamente la misma calidad de output pero podrás notar una diferencia de velocidad. Clic para ver ejemplo online y ver código de fuente.

output2

Estamos utilizando 2 Web Workers en este caso y podrás ver que el encoding del Gif tarda un poco en procesarse. Te invito a abrir la consola de tu navegador para ver cómo están trabajando. Mira la diferencia entre el Demo 2 y el Demo 3.

Hora de jugar

Ahora es tu turno. Recuerda que tienes todas las novedades que HTML5 te ofrece para poder interactuar con estas dos maravillosas librerías.

Para un pequeño prototipo que hice llamado TuitGif, realicé la integración con el repositorio de antimatter15 conectando el output de la cámara directamente al canvas y "grabando" los usuarios pueden obtener un Gif animado para utilizarlo en redes sociales. Esto es sólo un ejemplo de lo que se puede hacer, las posibilidades son infinitas - o hasta dónde llegue el poder de Javascript y el procesador del usuario.

Dudas, ideas y preguntas son totalmente bienvenidas en los comentarios o escríbeme un tweet.

Referencias

Alejandro AR

Junkie Web Developer y Artesano Digital, escribo de colores en varios lenguajes con mucho amor. Hablo Ruby, Javascript, PHP y mas, conozco al usuario y genero experiencias naturales.