Conociendo HTML5. El elemento canvas.

HTML5 está en boca de todos. A pesar de ser un standard todavía en fase de borrador, parece prometer una nueva era de aplicaciones web como nunca hasta ahora se habían visto, llenas de interactividad, efectos, e integración con el sistema de forma que sean casi como aplicaciones de escritorio.

Todo esto no es nuevo, pero HTML5 pretende ir más allá. Hasta ahora apenas hemos rascado la superficie de lo que HTML5 puede dar de sí, porque el standard define unas características y unas normas, pero depende de el ingenio de los desarrolladores tirar de la cuerda y sacar buenas ideas.

El principal freno para la adopción universal de todas las nuevas tecnologias que trae HTML5 consigo es la retrocompatibilidad de tu trabajo. Nadie quiere hacer sus webs para que las vean 4 gatos, y hay que cuidar a toda esa panda de desgraciados a todos los usuarios que se sienten felices usando internet explorer 6. Y 7… y 8.

Pero parece que vienen nuevos y mejores tiempos. La competencia ha ganado terreno e incluso internet explorer 9 pinta realmente bien y las primeras betas muestran un soporte realmente bueno de la nueva especificación. Ahora que no hay freno, es el momento de ir aprendiendo que es eso de microdata, canvas, websockets, <video>, ect…

Voy a empezar el tag <canvas></canvas>, que es para mi, junto con websockets, la auténtica revolución de la web.

Un tag canvas, en español, lienzo, es justamente una superficie delimitada en que tenemos absoluta libertad para dibujar en ella gráficos, de la forma que nos venga en gana. Así, como suena. No estamos limitados por las estructuras cuadradas de los divs, el flujo arriba->abajo e izquierda->derecha habitual. Todo lo que podamos modelar en forma de código, es representable. El único límite es la potencia de la máquina y nuestra capacidad. La parte mala de todo esto, es que es bastante mas complicado que escribir tags html y definir reglas css. Los gráficos por computación son un tema un poco árido, bastante matemático a veces, y todo ha de hacerse programando en javascript. Aun así, podemos lograr efectos bastante chulos sin demasiado esfuerzo.

En el tema de hoy planteo materializar una idea. Quiero lograr un efecto en que por acción del usuario, se genere anillo de borde difuminado que crezca y se haga transparente en una bonita animación. Algo parecido al efecto de la honda expansiva que genera una piedra al caer en el agua. Este es un problema que se me planteo de verdad hace unos dias, y el código que voy a poner es exactamente el código que yo estoy usando.

Vamos a empezar creando una web simple, con un tag <canvas> y un título, y vamos a incluirle la libreria de jQuery para hacer las cosas más vistosas. Voy a escribir el código directamente en el archivo html.

Queda asi:

<html>
<head>
  <script type="text/javascript" src="jquery.js"></script>
  <script type="text/javascript">
    /* Mi codigo irá aqui */
  </script>
</head>
<body>
<h1>Prueba de canvas</h1>
<canvas id="canvas" height="400" width="600" style="border: 1px solid gray;"></canvas>
</body>
</html>

Defino una altura y una anchura usando esos atributos, y no reglas CSS. Es mejor así. Tambien le pongo borde para que se vean bien sus límites.
El primer paso para dibujar en un canvas, es saber que los lienzos tienen contextos. Un contexto yo lo definiría como el pincel que nos permite dibujar en el lienzo. De forma más formal, es el objeto todopoderoso que nos expone la API de dibujo. Todo lo que dibujemos para por él. Puede ser de tipo 2D o 3D, pero vamos a quedarnos con dos dimensiones de momento.
Antes de lanzarnos a dibujar, conviene saber si el navegador soporta esta característica. Hay un truco bastante simple y efectivo, que es decirle al navegador que tiene que crear un elemento canvas en memoria y nos devuelva su contexto. Si es capaz de hacerlo, es evidente que la tecnologia es soportada, así que podemos seguir trabajando.

<html>
<head>
  <script type="text/javascript" src="jquery.js"></script>
  <script type="text/javascript">
    $(function(){
      if (!!document.createElement('canvas').getContext) {
        alert("El elemento canvas está soportado");
      } else {
        alert("Usas un navegador del período cretácico. Actualizate.")
      }
    });
  </script>
</head>
<body>
<h1>Prueba de canvas</h1>
<canvas id="canvas" height="400" width="600" style="border: 1px solid gray;"></canvas>
</body>
</html>

Ahora, vamos a obtener el contexto, y a dibujar un círculo. Previamente hay que definir las propiedades de la pincelada. No quiero que esté coloreado por dentro, solo quiero que tenga borde de 5px. Azul semitransparente está bien.
Hay que decirle al pincel (context) donde empieza el trazo. En el medio, se dibuja un circulo, llamado a la context.arc(coord_x_centro,coord_y_centro,radio,angulo_de_inicio, algulo_de_fin, sentido_horario?). Después declaramos el trazo como finalizado.

<script type="text/javascript">
    var context;
    $(function(){
      if (!!document.createElement('canvas').getContext) {
        context = $("canvas")[0].getContext("2d");
        context.strokeStyle = "rgba(0,0,255,0.5)";
        context.lineWidth = 5;
        context.beginPath();  // Comienzo a definir el trazo del pincel
        context.arc(150,150,50,0,Math.PI*2,true);
        context.closePath();  // Declaro el fin del trazo
        context.stroke();     // Le digo al contexto que se dibujen los bordes
        // context.fill();    Esta linea colorearía el interior del circulo
      } else {
        alert("Usas un navegador del período cretácico. Actualizate.")
      }
    });
</script>

Sencillo. Ahora hay que apretarse el ingenio para lo que viene. Quiero lograr un efecto de difuminado. Tras probar con las funciones de sombra gausiana que nos da el api, no daba logrado el efecto que pretendía conseguir de bordes difuminados. Pero se me ocurrió que podia simularla dibujando varios circulos iguales pero cada uno con un trazo un poco más grueso que el anterior, todos semitransparentes. Al superponerlos el centro queda de un color más opaco y los bordes cada vez más diluidos.
Evidentemente hay que mantener un orden creando librearias con funciones. Encapsulamos el dubujado del circulo para usarlo con una sola llamada.

  <script type="text/javascript">
    var context;
    function dibujarCirculo(strokeStyle,lineWidth,x,y,rad) {
      context.strokeStyle = strokeStyle;
      context.lineWidth = lineWidth;
      context.beginPath();  // Comienzo a definir el trazo del pincel
      context.arc(x,y,rad,0,Math.PI*2,true);
      context.closePath();  // Declaro el fin del trazo
      context.stroke();     // Le digo al contexto que se dibujen los bordes
    }
    $(function(){
      if (!!document.createElement('canvas').getContext) {
        context = $("canvas")[0].getContext("2d");
        dibujarCirculo("rgba(0,0,255,0.05)",5,150,150,50);
        dibujarCirculo("rgba(0,0,255,0.05)",8,150,150,50);
        dibujarCirculo("rgba(0,0,255,0.05)",11,150,150,50);
        dibujarCirculo("rgba(0,0,255,0.05)",14,150,150,50);
        dibujarCirculo("rgba(0,0,255,0.05)",17,150,150,50);
      } else {
        alert("Usas un navegador del período cretácico. Actualizate.")
      }
    });
  </script>

Juego a ajustar los parametros y…¡Perfecto! Esto es lo que quería lograr. Se puede lograr un efecto de degradado mas suave dibujando más circulos con diferencias de 1px de grosor de linea, pero creo que no necesito más detalle.
En el siguiente paso vamos a agrupar estos dibujos en otra funcion que dibuje un circulo difuminado permitiendo pasarle por parametros su posición y su radio, y el radio de difuminado que deseamos. Quiero además que cuando se dibuje un circulo difuminado, se borre el anterior. Esto lo hago con vistas a luego crear una animación: antes de dibujar el siguiente fotograma, salvo que se desee lograr algun efecto motion-blur, debe eliminarse el anterior.
El borrado del canvas se hace mediante la función clearRect(x1,y1,x2,y2), que, tal y como parece por su nombre, borra todo lo contenido en el rectángulo definido por esos 2 vértices. Quiero borrar todo el canvas, así que le paso el punto (0,0) y el punto (ancho,alto).

  <script type="text/javascript">
    var context;
    var ancho, alto;
    function dibujarCirculo(strokeStyle,lineWidth,x,y,rad) {
      context.strokeStyle = strokeStyle;
      context.lineWidth = lineWidth;
      context.beginPath();  // Comienzo a definir el trazo del pincel
      context.arc(x,y,rad,0,Math.PI*2,true);
      context.closePath();  // Declaro el fin del trazo
      context.stroke();     // Le digo al contexto que se dibujen los bordes
    }
    function dibujarCirculoDifuso(strokeStyle,lineWidthIni,lineWidthFin,dx,x,y,rad) {
      var deltaWidth = lineWidthFin - lineWidthIni;
      context.clearRect(0,0,ancho,alto);
      for (var i = 0; i <= deltaWidth; i = i + dx) {
        dibujarCirculo(strokeStyle,lineWidthIni + i,x,y,rad);
      }
    }
    $(function(){
      if (!!document.createElement('canvas').getContext) {
        var $canvas = $("canvas");
        context = $canvas[0].getContext("2d");
        ancho = $canvas.attr("width"); alto = $canvas.attr("height");
        dibujarCirculoDifuso("rgba(0,0,255,0.05)",5,17,3,150,150,50);
      } else {
        alert("Usas un navegador del período cretácico. Actualizate.")
      }
    });
  </script>

El anillo está bien, pero hay que reconocer que no es emocionante. Hay que darle movimiento. La función setInterval(fx,interval) nativa de javascript nos permite temporizar la tasa de refresco de dibujado, estableciento el numero de milisegundos entre fotograma y fotograma. A menor intervalo, más fluida es la animación pero más recursos del sistema consume. Por mi experiencia, con 15ms es más que suficiente para una absoluta suavidad, yo no bajaría de ahí. Incluso suelo poner más.

  <script type="text/javascript">
    var context;
    var ancho, alto;
    function dibujarCirculo(strokeStyle,lineWidth,x,y,rad) {
      context.strokeStyle = strokeStyle;
      context.lineWidth = lineWidth;
      context.beginPath();  // Comienzo a definir el trazo del pincel
      context.arc(x,y,rad,0,Math.PI*2,true);
      context.closePath();  // Declaro el fin del trazo
      context.stroke();     // Le digo al contexto que se dibujen los bordes
    }
    function dibujarCirculoDifuso(strokeStyle,lineWidthIni,lineWidthFin,dx,x,y,rad) {
      var deltaWidth = lineWidthFin - lineWidthIni;
      context.clearRect(0,0,ancho,alto);
      for (var i = 0; i <= deltaWidth; i = i + dx) {
        dibujarCirculo(strokeStyle,lineWidthIni + i,x,y,rad);
      }
    }
    function dibujarCirculoAnimado(strokeStyle,lineWidthIni,lineWidthFin,dx,x,y,rad,incrementoDeRadio) {
      var i = 0;
      setInterval(function(){
        dibujarCirculoDifuso(strokeStyle,lineWidthIni,lineWidthFin,dx,x,y,rad + (i * incrementoDeRadio) );
        i+=1;
      },20)
    }
    $(function(){
      if (!!document.createElement('canvas').getContext) {
        var $canvas = $("canvas");
        context = $canvas[0].getContext("2d");
        ancho = $canvas.attr("width"); alto = $canvas.attr("height");
        dibujarCirculoAnimado("rgba(0,0,255,0.05)",5,17,3,150,150,50,1);
      } else {
        alert("Usas un navegador del período cretácico. Actualizate.")
      }
    });
  </script>

Ahora que ya se mueve, tenemos que usar la imaginación para lograr definir la animación que queremos. Yo quiero que el anillo crezca desde 60px hasta 110px en un intervalo definido por mi, al tiempo que se va haciendo cada vez más transparente, y que haga eso dos veces, como si fuese un indicador de posición.
Mientras hago eso me doy cuenta de que es más practico definir un radio inicial, un radio final, y el nivel de difuminado como un unico valor en lugar de dos.
Encapsulo toda la animación convenientemente para tener mi libreria ordenada.
Y para hacerlo interactivo, disparo la animación donde el usuario haga click, capturando el evento.

 <script type="text/javascript">
    var context;
    var ancho, alto;
    function dibujarCirculo(strokeStyle, lineWidth, x, y, rad) {
      context.strokeStyle = strokeStyle;
      context.lineWidth = lineWidth;
      context.beginPath();  // Comienzo a definir el trazo del pincel
      context.arc(x,y,rad,0,Math.PI*2,true);
      context.closePath();  // Declaro el fin del trazo
      context.stroke();     // Le digo al contexto que se dibujen los bordes
    }
    function dibujarCirculoDifuso(strokeStyle, lineWidth, blur, intervalorDeDifuminado, x, y, rad) {
      context.clearRect(0,0,ancho,alto);
      for (var i = 0; i <= blur; i = i + intervalorDeDifuminado) {
        dibujarCirculo(strokeStyle, lineWidth + i, x, y, rad);
      }
    }
    function dibujarCirculoAnimado(strokeStyle, lineWidth, blur, intervaloDeDifuminado, duracion, x, y, radIni, radFin) {
      var periodo = 25; //ms
      var incrementoDeRadio = radFin - radIni;
      var numeroFotogramas = duracion/periodo;
      var incrementoPixelsPorFotograma = incrementoDeRadio / numeroFotogramas;
      var frameActual = 0;
      var timer = setInterval(function(){
        if (frameActual<numeroFotogramas) {
          dibujarCirculoDifuso(strokeStyle,lineWidth,blur,intervaloDeDifuminado,x,y, radIni + (frameActual * incrementoPixelsPorFotograma));
          frameActual+=1;
          context.globalAlpha = 1.1 - (frameActual/numeroFotogramas);
        } else {  // Finaliza la animación
          context.clearRect(0,0,ancho,alto);
          context.globalAlpha = 1;
          clearInterval ( timer );
        }
      },periodo);
    }
    function animacionDoble(strokeStyle, lineWidth, blur, intervaloDeDifuminado, duracion, x, y, radIni, radFin) {
      var f = (function(){ dibujarCirculoAnimado(strokeStyle, lineWidth, blur, intervaloDeDifuminado, duracion, x, y, radIni, radFin); });
      f();
      setTimeout(f, duracion + 200);
    }
    $(function(){
      if (!!document.createElement('canvas').getContext) {
        var $canvas = $("canvas");
        context = $canvas[0].getContext("2d");
        ancho = $canvas.attr("width"); alto = $canvas.attr("height");
        var f = $canvas.click(function(evt) {
          var offset = $canvas.offset();
          var x_pos = Math.floor(evt.pageX - offset.left);
          var y_pos = Math.floor(evt.pageY - offset.top);
          animacionDoble("rgba(0,0,255,0.05)",5,12,3,350,x_pos,y_pos,60,110);
        });
      } else {
        alert("Usas un navegador del período cretácico. Actualizate.")
      }
    });
  </script>

Listo. Una bonita animación interactiva. ¿¿Y qué se puede hacer con esto??
Pues por ejemplo, yo lo he integrado junto con google maps en una aplicación, haciendo que muestre gráficamente el radio de busqueda sobre el mapa.
Os enlazo a un pequeño video de youtube donde ver el efecto (perdonad que el video no sea muy fluido, problemas al hacer el screencast).

Si os interesa ahondar más en el tema, hay un tutorial excelente en inglés en el que se sigue paso a paso la programación de un sencillo videojuego usando solo canvas y javascript para ello.
http://billmill.org/static/canvastutorial/

Espero que os gustase el tema, porque es probable que vuelva a salir pronto.
Si teneis alguna duda o quereis que se escarbe más en algún tema, dejad un comentario y veremos que se puede hacer.

Hasta pronto.

Anuncios

, , , ,

  1. #1 por Michel el noviembre 6, 2012 - 2:58 pm

    muchas gracias, me parece muy interesante, tienes algun efecto para hacer que un conjunto de imagenes aparezcan como un puzzle? saludos

    • #2 por miguelcamba el noviembre 6, 2012 - 5:06 pm

      No lo tengo, pero estoy seguro de que debe hacer algún plugin que haga algo similar. Hacerlo desde cero no parece razonable. Es una tarea compleja.

  2. #3 por Xana el mayo 2, 2013 - 6:37 pm

    me he propuesto animar un canvas, y la verdad, esto ha sido de gran ayuda, gracias desde Asturias.

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

A %d blogueros les gusta esto: