Introducción al elemento canvas de HTML-5

Canvas: un lienzo para nuestra web

La especificación WHATWG del nuevo elemento de HTML-5 canvas contiene casi todo para empezar a hacernos una primera idea de lo que hace este componente. El elemento <canvas> podemos definirlo como un lienzo para presentar gráficos de diversos tipos manipulables mediante JavaScript.

Haré algunas pruebas básicas en esta página para recoger una primera idea. Luego intentaré aplicarlas a mi trazador de gráficas matemáticas que se basaba en poner puntos en pantalla mediante la inserción de elementos HTML en el DOM. Con canvas la velocidad en el proceso del trazado así como su calidad son apreciablemente superiores. Por lo tanto haré que esa aplicación trabaje con ambos modos, pues para los navegadores que no soporten canvas aún podrán funcionar con el modo anterior.

Los ejemplos de esta página han sido probados en Chrome 15.0, Firefox 8.0, Opera 11.52 y Safari 5.1 funcionando como se esperaba.

Y esto es lo primero que tenemos que aprender. Cómo detectar si el navegador soporta este elemento. Pero empecemos por el principio. A continuación declaramos un elemento <canvas>:

<canvas width="100" height="75">
    <span class="rojo">Elemento 
    <b>canvas</b> no soportado.</span>
</canvas>
    

Ejemplo:

Elemento canvas no soportado.

Los ejemplos en este documento están dentro de un cuadro con borde verde. Dentro a su vez ponemos el elemento canvas con un ancho y alto de 100 y 75 píxeles respectivamente. Le ponemos un borde azul para apreciarlo. Si el navegador puede presentar el elemento descartará todo lo que haya en el interior. En otro caso presentará la frase puesta "Elemento canvas no soportado" o lo que necesitemos (una imagen por ejemplo). Entonces el área con borde azul será nuestro lienzo, término que sería la traducción más apropiada para canvas.

El elemento necesita JavaScript para ser manejado. Por lo tanto también hemos de detectar si el navegador lo soporta. Veámos este ejemplo:

<canvas id="canvas-1" width="100" height="75">
    <span class="rojo">Elemento 
    <b>canvas</b> no soportado.</span>
</canvas>

<script>
    var canvas1 = document.getElementById("canvas-1");
    if (canvas1.getContext) {
        var cnv1 = canvas1.getContext("2d");
        cnv1.fillRect(10,10,50,50);
    }
</script>

Ejemplo:

Elemento canvas no soportado.

Ponemos el elemento <canvas> identificado con id="cnv-1" y luego a continuación ponemos un script para manejarlo. Lo primero es preguntar si ese elemento posee el método getContext. Los navegadores que no lo reconozcan no ejecutarán lo que está dentro. Para el resto se creará un contexto de dos dimensiones con getContext("2d"). Por último empezamos a hacer cosas con el lienzo. En ese ejemplo insertamos un rectángulo en la posición (10,10) píxeles, medida desde la esquina superior izquierda. El rectángulo se rellena con el color negro (por defecto) y tiene unas medidas de 50x50 píxeles. Esto se hace de forma automática con el método fillRect().

El contexto 2D del canvas

Con el método getContext("2d") cargamos un contexto de 2 dimensiones. Es posible cargar otros contextos como WEBGL que se usa para presentaciones en 3 dimensiones. Pero por ahora sólo voy a mirar lo de 2 dimensiones. Intentando resumir todo lo que se puede hacer en canvas, podemos empezar haciendo lo siguiente:

  1. Trazar formas simples (rectángulos)
  2. Trazar formas complejas (caminos)
  3. Escribir texto
  4. Insertar imágenes

Además podemos operar con esas formas e imágenes haciendo cosas como:

  • Transformaciones: escalar el tamaño, rotar, trasladar y transformar.
  • Composiciones: transparencias y operaciones de composición.
  • Colores y estilos: dar color a una línea o para el relleno de un rectángulo, usar gradientes de color y patrones de repetición de imágenes.
  • Estilos de líneas: grosores de las líneas, terminaciones, etc.
  • Sombras: de forma similar a las de CSS3.
  • Manipulación de píxeles: usando un array que representa el mapa de bits podemos manipular el lienzo punto a punto.

Formas simples y estilos básicos con canvas

Las formas simples que podemos trazar son rectángulos:

  • Con fillRect(x,y,w,h) trazamos un rectángulo lleno de color posicionado con la esquina superior izquierda en el punto (x,y) y de ancho y alto w y h respectivamente. El color por defecto es el negro.
  • Con strokeRect(x,y,w,h) trazamos un rectángulo en (x,y) y de medidas (w,h).
  • Con clearRect(x,y,w,h) borramos todo el lienzo del rectángulo señalado por la posición (x,y) y de medidas (w,h).

En este ejemplo pondremos otro canvas:

<canvas id="canvas-2" width="400" height="110">
    <span class="rojo">Elemento 
    <b>canvas</b> no soportado.</span>
</canvas>
<br />
<input type="button" value="borrar con clearRect()"
onclick="borraCnv2ClearRect()" />
<input type="button" value="borrar con width"
onclick="borraCnv2Width()" />
<input type="button" value="Volver a trazar"
onclick="trazarCnv2()" />

Además ponemos algunos botones para borrar el lienzo y para volver a trazar el contenido. El resultado es este:

Ejemplo:

Elemento canvas no soportado.

Se trata de cuatro formas de rectángulos. La primera es una forma llena con cnv2.fillRect(10,20,70,70). Previamente le damos color de relleno con cnv2.fillStyle="red". El valor de esta propiedad así como para strokeStyle es un valor CSS3 de color, tal como ya expuse en el tema CSS3 Colores. Por ejemplo, el segundo rectángulo traza solo el borde con cnv2.strokeStyle="rgba(0,255,0,0.5)" dándole un color verde con transparencia del 50%. El grosor del borde lo controlamos con cnv2.lineWidth=20. Las esquinas son rectas con cnv2.lineJoin="miter", pudiendo hacerlas biseladas con cnv2.lineJoin="bevel" o bien redondeadas con cnv2.lineJoin = "round". El script que hace eso es este:

<script>
    var canvas2 = document.getElementById("canvas-2");
    var cnv2 = null;
    if (canvas2.getContext) {
        cnv2 = canvas2.getContext("2d");
        trazarCnv2();
    }
    function trazarCnv2(){
        //Rectángulo lleno de color rojo
        cnv2.fillStyle = "red";
        cnv2.fillRect(10,20,70,70);
        //Rectángulo con esquinas rectas
        cnv2.strokeStyle = "rgba(0,255,0,0.5)";
        cnv2.lineJoin = "miter";
        cnv2.lineWidth = 20;
        cnv2.strokeRect(100,20,70,70);            
        //Rectángulo con esquinas biseladas
        cnv2.strokeStyle = "olive";
        cnv2.lineWidth = 20;
        cnv2.lineJoin = "bevel";
        cnv2.strokeRect(200,20,70,70);
        //Rectángulo con esquinas redondeadas
        cnv2.strokeStyle = "blue";
        cnv2.lineWidth = 20;
        cnv2.lineJoin = "round";
        cnv2.strokeRect(300,20,70,70);
    }
    function borraCnv2ClearRect(){
        cnv2.clearRect(0, 0, canvas2.width, canvas2.height);
    }
    function borraCnv2Width(){
        canvas2.width = canvas2.width;
    }
</script>

Tenemos un par de formas de borrar el lienzo. Una es con cnv2.clearRect(0,0,W,H) siendo W y H el ancho y ancho del elemento canvas, no del contexto. En el ejemplo el elemento está referenciado como canvas2 mientras que el contexto como cnv2. Otra forma de borrar el lienzo es modificando el ancho del elemento con canvas2.width=canvas2.width, en este caso al mismo valor que tenía (aunque esto no me ha funcionado con Opera 11.52).

Formas complejas: caminos con canvas

El contexto de un canvas tiene un único camino que a su vez se compone de cero o más subcaminos. Se define un subcamino como una sucesión de uno o más puntos conectados por líneas rectas o curvas y un indicador que establece si el subcamino está cerrado o no. Un subcamino está cerrado si el último punto se conecta con el primer punto mediante una línea recta. Los subcaminos de uno o ningún punto son ignorados y no se pintan en el lienzo.

Ejemplo:

Elemento canvas no soportado.

En este ejemplo tenemos otro canvas declarado como canvas3 y con el contexto cnv3 igual que el del ejemplo del apartado anterior. El código HTML no lo vamos a repetir pues es igual que el de ese ejemplo. El script es el siguiente:

var canvas3 = document.getElementById("canvas-3");
var cnv3 = null;
if (canvas3.getContext) {
    cnv3 = canvas3.getContext("2d");
    trazarCnv3();
}
function trazarCnv3(){
    //Un subcamino de dos rectas en V
    cnv3.beginPath();
    cnv3.moveTo(20,20);
    cnv3.lineTo(45,80);
    cnv3.lineTo(70,20);
    cnv3.lineWidth = 10;
    cnv3.lineJoin = "round";
    cnv3.lineCap = "round";
    cnv3.strokeStyle = "green";
    cnv3.stroke();
    cnv3.closePath();
    //Un subcamino rectángulo
    cnv3.beginPath();
    cnv3.rect(100,20,60,60);
    cnv3.lineWidth = 10;
    cnv3.strokeStyle = "red";
    cnv3.shadowColor = "silver";
    cnv3.shadowOffsetX = 5;
    cnv3.shadowOffsetY = 5;            
    cnv3.stroke();
    cnv3.closePath();
    //Un subcamino circular
    cnv3.beginPath();
    cnv3.arc(220,50,30,0,(-3/2)*Math.PI,true);
    cnv3.lineWidth = 2;
    cnv3.strokeStyle = "blue"
    cnv3.stroke();
    cnv3.closePath();
}
function borraCnv3ClearRect(){
    cnv3.clearRect(0, 0, canvas3.width, canvas3.height);
}
function borraCnv3Width(){
    canvas3.width = canvas3.width;
}

Ponemos tres subcaminos simples. Cada subcamino se inicia con cnv3.beginPath() y se cierra con cnv3.closePath(). Si no lo cerramos se unirá con una línea recta con el siguiente subcamino. El primero es un grupo de dos rectas en forma de "V". Con moveTo(x,y) posicionamos en un punto. Con lineTo(x,y) dibujamos una recta entre el último punto del subcamino y ese (x,y). Unimos los puntos entre las dos rectas con el estilo cnv3.lineJoin="round" que hará que se redondee la esquina. También redondeamos las terminaciones del subcamino con cnv3.lineCap="round", Cuando el camino está trazado lo pintamos con cnv3.stroke(). Cerramos ese subcamino con cnv3.closePath().

El siguiente subcamino es un rectángulo. Le aplicamos estilo de sombra con cnv3.shadowColor dándole unas dimensiones a la sombra horizontal y vertical con shadowOffsetX y shadowOffsetY. Las esquinas son redondeadas pues sigue aplicando el estilo cnv3.lineJoin="round". Por último dibujamos un círculo pero sólo para el espacio angular entre 0 y 3π/2. Aunque en este caso la dirección sería anti-reloj (anticlockwise con valor true), de tal forma que el rango sería entre 0 y -3π/2. Vea como la sombra declarada para el subcamino anterior sigue activa pues no la hemos cambiado y se aplica a este subcamino también.

Insertando texto en canvas

Podemos escribir texto dentro del lienzo. Con este ejemplo ponemos un nuevo canvas4 con el contexto cnv4. El HTML es:

<canvas id="canvas-4" width="400" height="200">
    <span class="rojo">Elemento 
    <b>canvas</b> no soportado.</span>
</canvas>
<br />
Texto: <input type="text" value="Canvas 1234" id="texto4" />
<input type="button" value="Volver a trazar"
onclick="trazarCnv4()" />

Se trata de pasar el texto del cuadro <input type="text"> dentro del canvas.

Ejemplo:

Elemento canvas no soportado.
Texto:

Este es el script:

var canvas4 = document.getElementById("canvas-4");
var cnv4 = null;
if (canvas4.getContext) {
    cnv4 = canvas4.getContext("2d");
    trazarCnv4();
}
function trazarCnv4(){
    //Estilo inicial
    cnv4.strokeStyle = "olive";
    cnv4.shadowOffsetX = 0;
    cnv4.shadowOffsetY = 0;  
    cnv4.lineWidth = 2;
    //Borramos canvas
    cnv4.clearRect(0,0,canvas4.width,canvas4.height);
    //Texto en el input      
    var texto = document.getElementById("texto4").value;            
    //Letra hueca
    cnv4.font = "italic bold 48px Arial";
    cnv4.strokeText(texto,10,60);
    //Letra maciza con sombra en un recuadro
    var x = 10;
    var y = 90;
    var alto = 50;
    var relleno = 10;
    cnv4.font = "italic bold " + alto +"px Times New Roman, sans-serif";
    var metrica = cnv4.measureText(texto);
    var anchoCaja = metrica.width+2*relleno;
    var altoCaja = alto+2*relleno;
    cnv4.strokeStyle = "blue";
    cnv4.lineWidth = 5;
    cnv4.lineJoin = "round";           
    cnv4.strokeRect(x,y,anchoCaja,altoCaja);
    cnv4.fillStyle = "red";
    cnv4.shadowColor = "rgba(0,255,190,0.5)";
    cnv4.shadowOffsetX = 2;
    cnv4.shadowOffsetY = 2;
    cnv4.fillText(texto, x+relleno, y+alto);
}

Ponemos un estilo inicial con color de línea cnv4.strokeStyle="olive", quitamos sombras y ancho de línea de 2px. Esto lo hacemos para cuando volvamos a trazar retome el estilo inicial para el primer texto. Luego extraemos la cadena de texto del <input type="text"> y tras configurar el estilo de fuente con cn4.font (estilo {font} según CSS), pintamos el texto con letra hueca mediante cnv4.strokeText(texto,10,60). La posición (x,y) en (10,60) es la del borde izquierdo de la caja de texto y la de la base de línea alfabética, determinadas por las posiciones iniciales de los atributos textAlign="start" y textBaseLine="alphabetic". Esto es un poco líoso pero puede verse una imagen en la especificación de WHATWG que aclara algo. De alguna forma poniendo (x,y)=(10,60) escribirá el texto a partir de x=10 hacia la derecha y desde y=60 hacia arriba, pues la base de línea tiene el valor alphabetic.

La segunda parte de este ejemplo se aplica a pintar el mismo texto con letra maciza, con sombra e insertada dentro de un rectángulo. Declaramos la esquina superior izquierda, el alto del rectángulo y el relleno (a modo de padding) para separar el texto de los bordes. El ancho de este rectángulo se determina usando el objeto cnv4.measureText(texto). Por ahora este objeto tiene sólo la propiedad width que nos devuelve el ancho que vendría a ocupar un determinado texto. Dibujamos el rectángulo a modo de caja de texto redondeando las esquinas. Luego especificamos el estilo para el texto y lo posicionamos con cnv4.fillText(texto, x+relleno, y+alto). Al cambiar el texto se ajustará automáticamente la caja.

Imágenes y transformaciones con canvas

En un canvas también podemos insertar imágenes. En este apartado pondremos una y haremos transformaciones con ella. Se trata de aplicar traslación, rotación y escalado.

Ejemplo:

Elemento canvas no soportado.



Extraer imagen aquí

El HTML es similar a los anteriores. Se agregan botones que ejecutan las funciones trasladarMas(), trasladarMenos(), escalarMas() etc. Por último hay un elemento <textarea id="ta-data"> y un elemento imagen <img id="img-data"> inicialmente vacío. Un botón nos permite extraer la imagen del canvas a ese elemento.

var canvas5 = document.getElementById("canvas-5");
var cnv5 = null;
if (canvas5.getContext) {
    cnv5 = canvas5.getContext("2d");
}
//1/4 del tamaño original de la imagen
var anchoImg = 120/4;
var altoImg = 156/4;
//Creamos un elemento imagen
var imagen = new Image();
imagen.src="ejemplos/eiffel.jpg";
//Cuando se cargue la imagen la primera vez
//la dibujamos en el lienzo
imagen.onload = function(){
    trazarCnv5();
}
//Dibujamos la imagen con las medidas reducidas
//desde la posición 0,0
function trazarCnv5(){
    cnv5.drawImage(imagen,0,0,anchoImg,altoImg);
}
function borrarCnv5(){
    cnv5.clearRect(0,0,canvas5.width,canvas5.height);
}
function escalarMas(){
    borrarCnv5();
    cnv5.scale(3/2,3/2);
    trazarCnv5();
}
function escalarMenos(){
    borrarCnv5();
    cnv5.scale(2/3,2/3);
    trazarCnv5();
}
function rotarMas(){
    borrarCnv5();
    cnv5.rotate(Math.PI/32);
    trazarCnv5();
}
function rotarMenos(){
    borrarCnv5();
    cnv5.rotate(-Math.PI/32);
    trazarCnv5();
}
function trasladarMas(){
    borrarCnv5();
    cnv5.translate(5,5);
    trazarCnv5();
}
function trasladarMenos(){
    borrarCnv5();
    cnv5.translate(-5,-5);
    trazarCnv5();
}
function extraerImagen(){
    var dataImage = cnv5.canvas.toDataURL("image/png");
    document.getElementById("ta-data").value = dataImage;
    document.getElementById("img-data").src = dataImage;
}

El script lo iniciamos cargando el contexto. Luego guardamos las dimensiones de la imagen original reducidas a 1/4. Creamos un elemento imagen del tipo objeto HTMLImageElement con el constructor new Image(), obteniendo el elemento <img> de la misma forma que si hubiésemos hecho document.createElement("img"). Pero ahora no vamos a cargar ese elemento en el DOM. Luego le ponemos el origen imagen.src y sólo cuando se ejecute el evento onload trazamos la imagen con cnv5.drawImage(). Se posiciona en (x,y)=(0,0) y con esas dimensiones.

A continuación están las funciones para borrar el lienzo y el resto para escalar más o menos en múltiplos de 3/2 y 2/3 respectivamente. Rotamos una fracción de 1/64 de la circunferencia, es decir en ángulos 2π/64 = π/32 radianes. La constante Math.PI se obtiene del módulo Math de Javascript. La última transformación traslada el conjunto más o menos 5 píxeles.

Podemos obtener todo el lienzo como una imagen. Para esto usamos la función cnv5.canvas.toDataURL("image/png"). Es una función del elemento canvas, no del contexto. La imagen será extraida como de tipo image/png, pero en forma de datos serializados en base64. Estos datos son texto plano que nos permite enviar una imagen dentro de un documento HTML y no como un recurso externo. Puede ver más sobre la codificación base64 en imagen con datos internos.En este ejemplo lo volcamos dentro del <textarea> para observar el contenido. Pero también se lo adjudicamos al atributo src del elemento <img> que se diferencia con borde rojo de puntos. Con el menú del navegador podemos entonces guardar o abrir esa imgen en otra ventana.


Y hasta aquí esta introducción a canvas. Queda bastante más que aprender. Aparte de profundizar en algunas cuestiones vistas aquí hay otras características como gradientes de color, manipulación a nivel de píxeles, extraer imagen en un archivo, matrices de transformación, patrones de repetición, guardar y recuperar estados del contexto y, sobre todo, técnicas para conseguir sacar el máximo provecho a este recurso.