Capítulo 7 Canvas

El elemento canvas proporciona un API para dibujar líneas, formas, imágenes, texto, etc en 2D, sobre el lienzo que del elemento. Este API ya está siento utilizado de manera exhaustiva, en la creación de fondos interactivos, elementos de navegación, herramientas de dibujado, juegos o emuladores. Éste elemento canvas es uno de los elementos que cuenta con una de las mayores especificaciones dentro de HTML5. De hecho, el API de dibujado en 2D se ha separado en un documento a parte.

Un ejemplo de lo que se puede llegar a crear, es la recreación del programa MS Paint incluido en Windows 95.

MS Paint desarrollado sobre canvas

Figura 7.1 MS Paint desarrollado sobre canvas

7.1 Elementos básicos

Para comenzar a utilizar el elemento canvas, debemos colocarlo en el documento. El marcado es extremadamente sencillo:

<canvas id="tutorial" width="150" height="150"></canvas>

Éste nos recuerda mucho al elemento img, pero sin los atributos src y alt. En realidad, el elemento canvas solamente tiene los dos atributos mostrados en el ejemplo anterior: width y height, ambos opcionales y que pueden establecerse mediante las propiedades DOM. Cuando estos dos atributos no se especifican, el lienzo (canvas) inicial será de 300px de ancho por 150px de alto. Este elemento, además y como muchos otros, pueden modificarse utilizando CSS. Podemos aplicar cualquier estilo, pero las reglas afectarán al elemento, no a lo dibujado en el lienzo.

En la actualidad, todos los navegadores son compatibles con el elemento canvas, la diferencia entre ellos radica en qué funcionalidades del API han implementado.

Ahora que el elemento canvas está definido en el documento, la manera de dibujar en él es a través de JavaScript. El primer paso es obtener el contexto de dibujado. <canvas> crea una superficie de dibujo de tamaño fijo que expone uno o más contextos de representación, que se utilizan para crear y manipular el contenido mostrado. En el contexto de representación 2D, (existe otro contexto de representación en 3D, llamado WebGL), el canvas está inicialmente en blanco y, para mostrar algo, es necesario el acceso de un script al contexto de representación para que pueda dibujar en él. El método DOM getContext sirve para obtener el contexto de representación y sus funciones de dibujo.

var canvas = document.getElementById('tutorial');
var ctx = canvas.getContext('2d');

El siguiente ejemplo dibujaría dos rectángulos que se cruzan, uno de los cuales tiene transparencia alfa.

<html>
  <head>
    <script type="application/javascript">
      window.onload = function() {
        var canvas = document.getElementById("canvas");
        if (canvas.getContext) {
          var ctx = canvas.getContext("2d");
          ctx.fillStyle = "rgb(200,0,0)";
          ctx.fillRect (10, 10, 55, 50);
          ctx.fillStyle = "rgba(0, 0, 200, 0.5)";
          ctx.fillRect (30, 30, 55, 50);
        }
      };
   </script>
  </head>
  <body>
    <canvas id="canvas" width="150" height="150"></canvas>
  </body>
</html>
Rectángulos semitransparentes en canvas

Figura 7.2 Rectángulos semitransparentes en canvas

7.2 Dibujar formas

Para empezar a dibujar formas, es necesario hablar primero de la cuadrícula del canvas o espacio de coordenadas. Normalmente, una unidad en la cuadrícula corresponde a un px en el lienzo. El punto de origen de esta cuadrícula se coloca en la esquina superior izquierda (coordenadas(0,0)). Todos los elementos se colocan con relación a este origen.

Desgraciadamente, canvas solamente admite una forma primitiva: los rectángulos, por lo que el resto de las formas deberán crearse mediante la combinación de una o más funciones.

Existen tres funciones que dibujan un rectángulo en el lienzo:

  • fillRect(x,y,width,height): dibuja un rectángulo relleno
.
  • strokeRect(x,y,width,height): dibuja un contorno rectangular
.
  • clearRect(x,y,width,height): borra el área especificada y hace que sea totalmente transparente.

Cada una de estas funciones tiene los mismos parámetros. x e y especifican la posición en el lienzo. width es la anchura y height la altura.

var canvas = document.getElementById('tutorial');
var ctx = canvas.getContext('2d');
ctx.fillRect(25,25,100,100);
ctx.clearRect(45,45,60,60);
ctx.strokeRect(50,50,50,50);

La función fillRect dibuja un gran cuadrado negro de 100x100 px. La función clearRect elimina un cuadrado de 60x60 px del centro y finalmente el strokeRect dibuja un contorno rectangular de 50x50 px en el interior del cuadrado despejado.

A diferencia de las funciones de rutas que veremos en la siguiente sección, las tres funciones de rectángulo se dibujan inmediatamente en el lienzo.

Dibujado de rectángulos en canvas

Figura 7.3 Dibujado de rectángulos en canvas

7.3 Rutas

Gracias al API 2D, es posible movernos a través del canvas y dibujar líneas y formas. Las rutas son utilizadas para dibujar formas (líneas, curvas, polígonos, etc) que de otra forma no podríamos conseguir.

El primer paso para crear una ruta es llamar al método beginPath. Internamente, las rutas se almacenan como una lista de subrutas (líneas, arcos, etc.) que, en conjunto, forman una figura. Cada vez que se llama a este método, la lista se pone a cero y podemos empezar a dibujar nuevas formas. El paso final sería llamar al método closePath: este método intenta cerrar la forma trazando una línea recta desde el punto actual hasta el inicial. Si la forma ya se ha cerrado o hay solo un punto en la lista, esta función no hace nada.

var canvas  = document.getElementById('tutorial');
var context = canvas.getContext('2d');
context.beginPath();
//... path drawing operations
context.closePath();

El siguiente paso es dibujar la forma como tal. Para ello, disponemos de algunas funciones de dibujado de líneas y arcos, que especifican las rutas a dibujar.

7.3.1 Método lineTo

Para dibujar líneas rectas utilizamos el método lineTo. Este método toma dos argumentos x e y, que son las coordenadas del punto final de la línea. El punto de partida depende de las rutas anteriores.

En el siguiente ejemplo se dibujan dos triángulos, uno relleno y el otro únicamente trazado. En primer lugar se llama al método beginPath para iniciar una nueva ruta. A continuación, utilizamos el método moveTo para mover el punto de partida hasta la posición deseada. Finalmente se dibujan dos líneas que forman dos lados del triángulo. Al llamar al método closePath, éste traza una línea al origen, completando el triángulo.

// Triángulo relleno
ctx.beginPath();
ctx.moveTo(25,25);
ctx.lineTo(105,25);
ctx.lineTo(25,105);
ctx.closePath();
ctx.fill();
// Triángulo trazado
ctx.beginPath();
ctx.moveTo(125,125);
ctx.lineTo(125,45);
ctx.lineTo(45,125);
ctx.closePath();
ctx.stroke();

En ambos casos utilizamos dos funciones de pintado diferentes: stroke y fill. Stroke se utiliza para dibujar una forma con contorno, mientras que fill se utiliza para pintar una forma sólida.

7.3.2 Arcos

Para dibujar arcos o círculos se utiliza el método arc (la especificación también describe el método arcTo). Este método toma cinco parámetros: x e y, el radio, startAngle y endAngle (que definen los puntos de inicio y final del arco en radianes) y anticlockwise (un valor booleano que, cuando tiene valor true dibuja el arco de modo levógiro y viceversa cuando es false).

Un ejemplo algo más complejo que los anteriores utilizando el método arc sería:

for(var i=0;i<4;i++){
    for(var j=0;j<3;j++){
        ctx.beginPath();
        var x              = 25+j*50;       // coordenada x
        var y              = 25+i*50;       // coordenada y
        var radius         = 20;            // radio del arco
        var startAngle     = 0;             // punto inicial del círculo
        var endAngle       = Math.PI+(Math.PI*j)/2; // punto final
        var anticlockwise  = i%2==0 ? false : true;
        ctx.arc(x,y,radius,startAngle,endAngle, anticlockwise);
        if (i>1){
            ctx.fill();
        } else {
            ctx.stroke();
        }
    }
}
Dibujado de arcos en canvas

Figura 7.4 Dibujado de arcos en canvas

Nota

Los métodos arc, bezier y quadratic utilizan radianes, pero si preferimos trabajar en grados, es necesario convertirlo a radianes. Esta el la operación que tenemos que llevar a cabo:

var radians = degrees * Math.PI / 180;

7.3.3 Método moveTo

Disponemos de la función moveTo, que en realidad, aunque no dibuja nada, podemos imaginárnosla como 'si levantas un lápiz desde un punto a otro en un papel y lo colocas en el siguiente'. Esta función es utilizada para colocar el punto de partida en otro lugar o para dibujar rutas inconexas.

ctx.beginPath();
ctx.arc(75,75,50,0,Math.PI*2,true);  // círculo exterior
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#FFF';
ctx.beginPath();
ctx.arc(75,75,35,0,Math.PI,false);   // boca (dextrógiro)
ctx.closePath();
ctx.fill();
ctx.moveTo(65,65);
ctx.beginPath();
ctx.arc(60,65,5,0,Math.PI*2,true);  // ojo izquierdo
ctx.closePath();
ctx.fill();
ctx.moveTo(95,65);
ctx.beginPath();
ctx.arc(90,65,5,0,Math.PI*2,true);  // ojo derecho
ctx.closePath();
ctx.fill();
Dibujar una cara sonriente en canvas

Figura 7.5 Dibujar una cara sonriente en canvas

7.4 Colores

Si queremos aplicar colores a una forma, hay dos características importantes que podemos utilizar: fillStyle y strokeStyle.

fillStyle = color
strokeStyle = color

strokeStyle se utiliza para configurar el color del contorno de la forma y fillStyle es para el color de relleno. Color puede ser una cadena que representa un valor de color CSS, un objeto degradado o un objeto modelo.

// todos ellos configuran fillStyle a 'naranja' (orange)
ctx.fillStyle = "orange";
ctx.fillStyle = "#FFA500";
ctx.fillStyle = "rgb(255,165,0)";
ctx.fillStyle = "rgba(255,165,0,1)";

Ejemplo de fillStyle:

function draw() {
    for (var i=0;i<6;i++){
        for (var j=0;j<6;j++){
            ctx.fillStyle = 'rgb(' + Math.floor(255-42.5*i) + ','
                                    + Math.floor(255-42.5*j) + ',0)';
            ctx.fillRect(j*25,i*25,25,25);
        }
    }
}

Ejemplo de strokeStyle:

function draw() {
    for (var i=0;i<6;i++){
        for (var j=0;j<6;j++){
            ctx.strokeStyle = 'rgb(0,' + Math.floor(255-42.5*i)
                                        + ',' +  ath.floor(255-42.5*j) + ')';
            ctx.beginPath();
            ctx.arc(12.5+j*25,12.5+i*25,10,0,Math.PI*2,true);
            ctx.stroke();
        }
    }
}

7.5 Degradados y patrones

A través del contexto, es posible generar degradados lineales, radiales o rellenos a través de patrones, que pueden ser utilizados en el método fillStyle del canvas. Los degradados funcionan de una manera similar a los definidos en CSS3, donde se especifican el inicio y los pasos de color para el degradado.

Los patrones, por otra parte, permiten definir una imagen como origen y especificar el patrón de repetido, de nuevo de manera similar a como se realizaba con la propiedad background-image de CSS. Lo que hace interesante al método createPattern es que como origen podemos utilizar una imagen, un canvas o un elemento de vídeo.

Un simple gradiente se crea de la siguiente manera:

var canvas = document.getElementById('tutorial');
var ctx = canvas.getContext('2d');
var gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
gradient.addColorStop(0, '#fff');
gradient.addColorStop(1, '#000');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);

El código anterior creamos un degradado lineal al que aplicamos dos pasos de color. Los argumentos de createLinearGradient son el punto de inicio del degradado (x1 e y1) y el punto final del degradado (x2 e y2). En este caso el degradado comienza en la esquina superior izquierda, y termina en la esquina inferior izquierda. Utilizamos este degradado para pintar el fondo de un rectángulo.

Degradado lineal

Figura 7.6 Degradado lineal

Los degradados radiales son muy similares, con la excepción que definimos el radio después de cada coordenada:

var canvas = document.getElementById('tutorial');
var ctx = canvas.getContext('2d');
gradient = ctx.createRadialGradient(canvas.width/2,
                                    canvas.height/2,
                                    0,
                                    canvas.width/2,
                                    canvas.height/2,
                                    150);
gradient.addColorStop(0, '#fff');
gradient.addColorStop(1, '#000');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.width);

La única diferencia es la manera en la que se ha creado el degradado. En este ejemplo, el primer punto del degradado se define en el centro del canvas, con radio cero. El siguiente punto se define con un radio de 150px pero su origen es el mismo, lo que produce un degradado circular.

Degradado radial

Figura 7.7 Degradado radial

Los patrones son incluso más sencillos de utilizar. Es necesaria una fuente (como una imagen, un canvas o un elemento de vídeo) y posteriormente utilizamos esta fuente en el método createPattern y el resultado de éste en el método fillStyle. La única consideración a tener en cuenta es que, al utilizar elementos de imagen o vídeo, éstos tienen que haber terminado de cargarse para poder utilizarlos. En el siguiente ejemplo, expandimos el canvas para que ocupe toda la ventana, y cuando se carga la imagen, la utilizamos como patrón de repetición.

var canvas = document.getElementById('tutorial');
var img = document.createElement('img');
var ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
img.onload = function () {
    ctx.fillStyle = ctx.createPattern(this, 'repeat');
    ctx.fillRect(0, 0, canvas.width, canvas.height);
};
img.src = 'avatar.jpg';
Uso de patrones en canvas

Figura 7.8 Uso de patrones en canvas

7.6 Transparencias

Además de dibujar formas opacas en el lienzo, también podemos dibujar formas semitransparentes. Esto se hace mediante el establecimiento de la propiedad globalAlpha o podríamos asignar un color semitransparente al trazo y/o al estilo de relleno.

globalAlpha = transparency value

Esta propiedad aplica un valor de transparencia a todas las formas dibujadas en el lienzo y puede ser útil si deseas dibujar un montón de formas en el lienzo con una transparencia similar; ya que debido a que las propiedades strokeStyle y fillStyle aceptan valores de color CSS3, podemos utilizar la siguiente notación para asignarles un color transparente.

function draw() {
    // dibujar fondo
    ctx.fillStyle = '#FD0';
    ctx.fillRect(0,0,75,75);
    ctx.fillStyle = '#6C0';
    ctx.fillRect(75,0,75,75);
    ctx.fillStyle = '#09F';
    ctx.fillRect(0,75,75,75);
    ctx.fillStyle = '#F30';
    ctx.fillRect(75,75,150,150);
    ctx.fillStyle = '#FFF';
    // establecer valor de transparencia
    ctx.globalAlpha = 0.2;
    // Dibujar círculos semitransparentes
    for (var i=0;i<7;i++){
        ctx.beginPath();
        ctx.arc(75,75,10+10*i,0,Math.PI*2,true);
        ctx.fill();
    }
}

7.7 Transformaciones

Al igual que tenemos la posibilidad mover el lápiz por el canvas con el método moveTo, podemos definir algunas transformaciones como rotación, escalado, transformación y traslación (similares a las conocidas de CSS3).

7.7.1 Método translate

Éste método traslada el centro de coordenadas desde su posición por defecto (0, 0) a la posición indicada.

ctx.translate(x, y);

7.7.2 Método rotate

Éste método inicia la rotación desde su posición por defecto (0,0). Si se rota el canvas desde esta posición, el contenido podría desaparecer por los límites del lienzo, por lo que es necesario definir un nuevo origen para la rotación, dependiendo del resultado deseado.

ctx.rotate(angle);

Éste método sólo precisa tomar un parámetro, que es el ángulo de rotación que se aplicará al marco. Este parámetro es una rotación dextrógira medida en radianes.

function draw() {
    ctx.translate(75,75);
    for (i=1;i<6;i++){
        // Desplazarse or los anillos (desde dentro hacia fuera)
        ctx.save();
        ctx.fillStyle = 'rgb('+(51*i)+','+(255-51*i)+',255)';
        for (j=0;j<i*6;j++){
            // dibujar puntos individuales
            ctx.rotate(Math.PI*2/(i*6));
            ctx.beginPath();
            ctx.arc(0,i*12.5,5,0,Math.PI*2,true);
            ctx.fill();
        }
        ctx.restore();
    }
}

7.7.3 Método scale

El siguiente método de transformación es el escalado. Se utiliza para aumentar o disminuir las unidades del tamaño de nuestro marco. Este método puede usarse para dibujar formas ampliadas o reducidas.

ctx.scale(x, y);

x e y definen el factor de escala en la dirección horizontal y vertical respectivamente. Los valores menores que 1.0 reducen el tamaño de la unidad y los valores mayores que 1.0 aumentan el tamaño de la unidad. Por defecto, una unidad en el área de trabajo equivale exactamente a un píxel. Si aplicamos, por ejemplo, un factor de escalado de 0.5, la unidad resultante será 0.5 píxeles, de manera que las formas se dibujarán a mitad de su tamaño.

7.8 Animaciones

Como estamos utilizando scripts para controlar los elementos canvas, resulta muy fácil hacer animaciones (interactivas). Sin embargo, el elemento canvas no fue diseñado para ser utilizado de esta manera (a diferencia de flash) por lo que existen limitaciones.

Probablemente, la mayor limitación es que una vez que se dibuja una forma, se queda de esa manera. Si necesitamos moverla, tenemos que volver a dibujar dicha forma y todo lo que se dibujó anteriormente.

Los pasos básicos a seguir son los siguientes:

  1. Borrar el lienzo: a menos que las formas que se dibujen llenen el lienzo completo (por ejemplo, una imagen de fondo), se necesitará borrar cualquier forma que se haya dibujado con anterioridad. La forma más sencilla de hacerlo es utilizando el método clearRect.
  2. Guardar el estado de canvas: si se va a modificar alguna configuración (estilos, transformaciones) que afectan al estado de canvas.
  3. Dibujar formas animadas: representación del marco.
  4. Restaurar el estado de canvas: restaurar el estado antes de dibujar un nuevo marco.

7.8.1 Control

Las formas se dibujan en el lienzo mediante el uso de los métodos canvas directamente o llamando a funciones personalizadas. Necesitamos una manera de ejecutar nuestras funciones de dibujo en un período de tiempo. Hay dos formas de controlar una animación como ésta. En primer lugar está las funciones setInterval y setTimeout, que se pueden utilizar para llamar a una función específica durante un período determinado de tiempo.

setInterval (animateShape, 500);
setTimeout (animateShape, 500);

El segundo método que podemos utilizar para controlar una animación es el input del usuario. Si quisiéramos hacer un juego, podríamos utilizar los eventos de teclado o de ratón para controlar la animación. Al establecer EventListeners, capturamos cualquier interacción del usuario y ejecutamos nuestras funciones de animación.

Ejercicio 7

Ver enunciado