Tutorial de Canvas HTML: Qué es y cómo se utiliza

html

En este tutorial veremos cómo utilizar la etiqueta canvas de HTML, utilizada para dibujar elementos gráficos en pantalla. Para seguir este tutorial, puedes crear los archivos a los que hago referencia localmente o usar alguna aplicación online como codepen.

Contenidos

Qué es un canvas

Un canvas es por definición un lienzo sobre el cual podemos dibujar o crear elementos gráficos. La etiqueta canvas de HTML permite agregar un lienzo de unas dimensiones definidas en el cual podremos dibujar elementos gráficos mediante JavaScript.

La API canvas que incluyen los navegadores web permite agregar líneas, imágenes, texto, formas geométricas y muchos otros elementos. A día de hoy existen librerías y motores gráficos que usan la API canvas como base para generar tanto gráficos 2D como 3D. Un ejemplo de ello son three.js, PixiJS o Phaser.

A diferencia de la etiqueta svg, que trabaja con gráficos vectoriales, las imágenes de un canvas están formadas por pixels.

Cómo crear un canvas

Parara crear un canvas en un documento HTML debes utilizar la etiqueta canvas, que consta de una etiqueta de apertura y de otra de cierre. Crea un nuevo documento HTML llamado index.html en una carpeta y agrega la siguiente etiqueta:

<canvas id="mi-canvas"></canvas>

Tras agregar la etiqueta anterior todavía no verás nada, ya que el borde del canvas agregado es invisible. Vamos a agregar un borde al mismo mediante CSS. Crea un directorio llamado /css y crea el archivo estilos.css en su interior con el siguiente código, en el que también hemos eliminado el margen que agregan algunos navegadores por defecto al body:

body {
  margin: 0;
}
#mi-canvas {
  border: 1px solid black;
}

Luego agrega una referencia al archivo estilos.css en el head del documento HTML:

<link rel="stylesheet" href="css/estilos.css">

Ahora crearemos también un archivo JavaScript, en el cual crearemos la diferentes manipulaciones del canvas. Para ello crea el directorio /js y en su interior el archivo index.js. Luego agrega el archivo en el head del archivo HTML:

<script type="text/javascript" src="js/index.js"></script>

Para seleccionar el canvas que hemos creado y comenzar a agregar elementos, bastará con seleccionarlo usando el siguiente selector de JavaScript:

const miCanvas = document.getElementById('mi-canvas');

Cómo modificar un canvas

Una vez creado el canvas, puede que quieras cambiar su tamaño o su color de fondo. A continuación vamos a ver cómo personalizarlo.

Cómo cambiar el color de un canvas

Para cambiar el color de fondo de un canvas, basta con usar la propiedad background-color de CSS:

#mi-canvas {
  background-color: #33FFD4;
  border: 1px solid black;
}

Y con esto ya habremos cambiado el color de fondo.

A continuación vamos a redimensionarlo para que ocupe toda la pantalla.

Cómo redimensionar un canvas

Podemos cambiar el tamaño de un canvas usando CSS, mediante las propiedades width y height. En el siguiente ejemplo vamos a hacer que ocupe todo el ancho y todo el alto de la pantalla. Además, vamos a eliminar el borde que habíamos agregado:

#mi-canvas {
  background-color: #33FFD4;
  border: 1px solid black;
  box-sizing: border-box;
  width: 100vw;
  height: 100vh;
}

Información! Las unidades vw y vh representan el porcentaje de la altura y anchura del viewport respectivamente.
Ahora el canvas ocupará todo el ancho y todo el alto de la pantalla siempre y cuando sea descendiente directo de la etiqueta body. En caso de que no sea así, dependiendo del navegador, también puedes hacer que el canvas ocupe toda la pantalla mediante JavaScript:

 

const miCanvas = document.getElementById('mi-canvas');
miCanvas.width = window.innerWidth;
miCanvas.height = window.innerHeight;

Este sería el resultado:

Si estableces el tamaño del canvas mediante JavaScript, tendrás que volver a calcular sus dimensiones cada vez que se redimensione la ventana. Puedes hacerlo mediante el evento resize, asignando al canvas el mismo tamaño que la ventana.

window.addEventListener('resize', () => {
  miCanvas.width = window.innerWidth;
  miCanvas.height = window.innerHeight;
});

Sin embargo, cuando son muchos los elementos que hay en el canvas, es recomendable limitar la frecuencia con la que se actualiza el tamaño del canvas para evitar problemas de rendimiento. Podemos lograrlo mediante un temporizador que ejecute los métodos que cambian el tamaño del mismo cada 200ms, por poner un ejemplo:

const funcionTemporizada = (func) => {
  let temporizador;
  return (event) => {
    if (temporizador) {
      clearTimeout(temporizador)
    }
    temporizador = setTimeout(func, 200, event);
  }
}
window.addEventListener('resize', funcionTemporizada(() => { 
  miCanvas.width = window.innerWidth;
  miCanvas.height = window.innerHeight;
}));

Contexto de renderizado

Antes de comenzar a dibujar en el canvas necesitas seleccionar el contexto de renderizado, que son el conjunto de métodos y propiedades que utilizarás para renderizar gráficos en el canvas. En cada canvas solamente puede usarse un único contexto.

Para seleccionar el contexto debes usar el método getContext(), que acepta los siguientes posibles valores:

  • contexto 2d: Se trata de un contexto de rederizado en dos dimensiones (más).
  • contexto webgl: Contexto 3D que usa la primera versión de la librería WebGL (más).
  • contexto webgl2: Contexto 3D que usa la segunda versión de la librería WebGL (más).
  • contexto bitmaprender: Interfaz que representa imágenes como mapas de bits (más).

Información! La librería WebGL es una especificaicón que define un API JavaScript compatible con OpenGL capaz de renderizar elementos en 3D en un navegador web.
En el siguiente ejemplo seleccionamos un contexto 2d:

 

const contexto = miCanvas.getContext('2d');

Dependiendo del contexto, el método getContext() puede aceptar más parámetros. En el caso del contexto 2d, el segundo parámetro es una variable boolena que permite indicarle al contexto si el fondo del canvas tiene transparencia o no. Su valor por defecto es true, aunque si su valor es false, indicaremos el navegador que el canvas no tiene un fondo transparente, por lo que será capaz de renderizar el canvas más rápido.

Cómo dibujar elementos en el canvas

Una vez seleccionado el contexto del canvas, ya podremos dibujar o agregar elementos de diferentes tipos, como texto, líneas, rectángulos, formas personalizadas o imágenes. Además, podrás personalizar el color o el gradiente de dichos elementos, así como el color del borde, su patrón o su sombra, tal y como veremos más adelante.

También podrás realizar otras operaciones con ellos que cambiarán su aspecto, ya que podrás rotarlos, escalarlos o deformarlos, entre muchas otras. A continuación vamos a ver los diferentes métodos que podemos usar para dibujar.

Cómo dibujar líneas

Para dibujar líneas puedes usar los métodos beginPath, moveTo y stroke, entre otros. Vamos a ver un ejemplo de cada uno de estos métodos por separado.

beginRect: Comienza a dibujar una línea

El método beginPath se usa para indicar que se ha comenzado a dibujar una línea, pero no dibujará nada en el canvas hasta que no realicemos más acciones:

contexto.beginPath();

No se debe volver a utilizar este método hasta haber terminado el dibujo actual, salvo que se quieran descartar los cambios.

moveTo: mueve el cursor a una posición

El método moveTo(x, y) se utiliza para mover el cursor de dibujo a la posición (x, y) indicada, sin dibujar nada el el canvas:

contexto.beginPath();
contexto.moveTo(50, 50);

lineTo: traza una línea

El método lineTo(x, y) se utiliza para trazar una línea desde la posición actual hasta la posición (x, y), sin dibujarla todavía en el canvas:

contexto.beginPath();
contexto.moveTo(50, 50);
contexto.lineTo(150, 150);

Este método puede usarse varias veces, trazando línea tras línea o incluso moviéndose a otra posición mediante el método moveTo para trazar más líneas en otro punto.

stroke: dibuja las líneas trazadas

El método stroke() simplemente dibujará los elementos ya definidos:

contexto.beginPath();
contexto.moveTo(50, 50);
contexto.lineTo(150, 150);
contexto.lineTo(350, 50);
contexto.stroke();

Este sería el resultado, ya visible en en el canvas:

closePath: cierra el conjunto de líneas

El método closePath() se utiliza para crear una línea que regrese al punto de origen, cerrando el conjunto. Una vez cerrado, es necesario usar el método stroke() para dibujar la línea:

contexto.beginPath();
contexto.moveTo(50, 50);
contexto.lineTo(150, 150);
contexto.lineTo(350, 50)
contexto.closePath();
contexto.stroke();

Este sería el resultado del ejemplo anterior:

quadraticCurveTo: traza una curva cuadrática

El método quadraticCurveTo(pcx, pcy, x, y) traza una curva cuadrática desde el punto actual hasta el punto (x, y). Los parámetros pcx y pcy representan la posición del punto de control que modifica la curva:

contexto.beginPath();
contexto.moveTo(50, 50);
contexto.quadraticCurveTo(50, 130, 230, 50);
contexto.stroke();

El resultado sería el siguiente, en el que hemos agregado también el punto (pcx, pcy) con fines descriptivos:

biezerCurveTo: traza una curva Biézer

El método biezerCurveTo(pc1x, pc1y, pc2x, pc2y, x, y) traza una curva Biézer desde el punto actual hasta el punto (x, y). Los puntos (pc1x, pc1y) y (pc2x, pc2y) representan la posición de los puntos de control de la curva:

contexto.beginPath();
contexto.moveTo(50, 50);
contexto.bezierCurveTo(50, 130, 230, 130, 230, 50); 
contexto.stroke();

El resultado sería el siguiente, en el que hemos agregado también los puntos (pc1x, pc1y) y (pc2x, pc2y)  con fines descriptivos:

arc: traza un arco

El método arc(x, y, r, iniAng, finAng, contrasentido) se usa para trazar un arco con radio r cuyo centro será el punto (x, y). Los parámetros iniAng y finAng indican el ángulo de inicio y el de finalización respectivamente, en radianes.

El parámetro contrasentido es opcional, aceptando los valores true o false. Si el valor del parámetro contrasentido es true, el arco se trazará en el sentido contrario a las agujas del reloj. Pero si es false se trazará en el sentido de las agujas del reloj, que es su valor por defecto.

En el siguiente ejemplo vamos a trazar un círculo:

contexto.beginPath();
contexto.arc(200, 175, 100, 0, 2 * Math.PI);
contexto.stroke();

El resultado sería el siguiente, en el que hemos agregado también el punto (x, y) y el radio r con fines descriptivos:

arcTo: traza un arco entre dos tangentes

El método arcTo(x1, y1, x2, y2, r) sirve para crear un arco entre dos tangentes que parten desde el punto actual y van hasta los puntos (x1, y1) la una y (x2, y2) la otra. El radio de la tangente viene representado por el parámetro r. Veamos un ejemplo:

contexto.beginPath();
contexto.moveTo(50, 50);
contexto.arcTo(180, 50, 180, 100, 50);
contexto.stroke();

Este sería el resultado:

isPointInPath: indica si un punto está en el path

El método isPointInPath(x, y) se utiliza para saber si el punto (x, y) está en el path o trazo actual, sin que necesariamente se haya dibujado. Devolverá true si se ha trazado algo en dicho punto o false de lo contrario:

contexto.rect(30, 30, 150, 150);
if (contexto.isPointInPath(50, 50)) {
  contexto.stroke();
};

Este sería el resultado, en el que se dibuja el rectángulo, dado que el método isPointInPath devuelve true:

Cómo personalizar las líneas

Existen una serie de propiedades que permiten personalizar ciertos aspectos de las líneas, como sus extremos o sus puntos de unión. A continuación vamos a ver cómo se utilizan.

lineWith: establece el ancho de las líneas

La propiedad lineWidth permite establecer la anchura de las líneas, aceptando como valor el ancho de las mismas en pixels:

contexto.lineWidth=10;
contexto.beginPath()
contexto.moveTo(100, 100);
contexto.lineTo(300, 100);
contexto.stroke();

contexto.lineWidth=20;
contexto.beginPath()
contexto.moveTo(100, 200);
contexto.lineTo(300, 200);
contexto.stroke();

Este sería el resultado:

lineCap: Establece la terminación de las líneas

La propiedad lineCap permite configurar el tipo de terminación de las líneas, aceptando uno de los siguientes valores:

  • butt: No se agregará ninguna terminación.
  • round: Se agregará una terminación circular.
  • square: Se agregará una terminación cuadrada.

Vamos a ver un ejemplo de cada terminación:

contexto.lineWidth=8;

contexto.beginPath()
contexto.lineCap='butt';
contexto.moveTo(100,20);
contexto.lineTo(200,200);
contexto.stroke();

contexto.beginPath()
contexto.moveTo(200,20);
contexto.lineCap='round';
contexto.lineTo(300,200);
contexto.stroke();

contexto.beginPath()
contexto.moveTo(300,20);
contexto.lineCap='square';
contexto.lineTo(400,200);
contexto.stroke();

Este sería el resultado:

lineJoin: establece el nexo de las líneas

La propiedad lineJoin permite configurar la junta o nexo de las líneas cuando dos o más líneas se cruzan, aceptado uno de los siguientes valores:

  • miter: El nexo será afilado, que es el valor por defecto.
  • round: Se agregará un nexo redondeado.
  • bevel: Se agregará un bisel o borde recortado.

Vamos a ver un ejemplo de cada nexo:

contexto.lineWidth=20;

contexto.beginPath()
contexto.lineJoin='miter';
contexto.moveTo(100,100);
contexto.lineTo(200, 150);
contexto.lineTo(200,20);
contexto.stroke();


contexto.beginPath()
contexto.lineJoin='round';
contexto.moveTo(300,100);
contexto.lineTo(400, 150);
contexto.lineTo(400,20);
contexto.stroke();

contexto.beginPathbevel
contexto.lineJoin='bevel';
contexto.moveTo(500,100);
contexto.lineTo(600, 150);
contexto.lineTo(600,20);
contexto.stroke();

Este sería el resultado:

miterLimit: establece el límite del inglete

La propiedad miterLimit sirve para establecer la longitud máxima en pixels que hay entre la esquina interior y al exterior entre las juntas cuando dos o más líneas se cruzan:

contexto.lineWidth=10;

contexto.beginPath();
contexto.lineJoin='miter';
contexto.miterLimit = 4;
contexto.moveTo(100, 30);
contexto.lineTo(130, 37);
contexto.lineTo(100, 44);
contexto.stroke();


contexto.beginPath();
contexto.miterLimit = 5;
contexto.moveTo(200, 30);
contexto.lineTo(230, 37);
contexto.lineTo(200, 44);
contexto.stroke();

Este sería el resultado:

Cómo dibujar rectángulos

Para dibujar un rectángulo puedes usar las funciones clearRect, fillRect o strokeRect, que aceptan los parámetros x (posición en el eje x), y (posición en el eje y), ancho y alto. Todas las unidades de los parámetros se especifican en pixels.

Vamos a ver un ejemplo de cada uno de estos métodos por separado.

rect: rectángulo

El método rect(x, y, ancho, alto) sirve para crear un rectángulo sin color de relleno. Sin embargo, este rectángulo no se dibujará en el canvas hasta que ejecutemos el método stroke(). En el siguiente ejemplo vamos a dibujar un rectángulo de 200px x 100px en la posición (200px, 100px):

contexto.lineWidth=10;
contexto.rect(200, 100, 200, 100);
contexto.stroke();

Esto dará como resultado el siguiente rectángulo:

fillRect: rectángulo con relleno

El método fillRect(x, y, ancho, alto) sirve para dibujar un rectángulo con el color de relleno seleccionado, que por defecto es el color negro. En el siguiente ejemplo vamos a dibujar un rectángulo de 200px x 100px en la posición (200px, 200px):

contexto.fillRect(200, 200, 200, 100);

Esto dará como resultado el siguiente rectángulo:

strokeRect: rectángulo con borde

El método strokeRect(x, y, ancho, alto) sirve para dibujar un rectángulo transparente cuyo borde será del color seleccionado, que por defecto es el color negro. En el siguiente ejemplo vamos a dibujar un rectángulo con esta herramienta de 200px x 100px en la posición (200px, 100px):

contexto.lineWidth=10;
contexto.strokeRect(200, 100, 200, 100);

Esto dará como resultado el siguiente rectángulo:

clearRect: borrado rectangular

El método clearRect(x, y, ancho, alto) sirve para eliminar un área rectangular del canvas, mostrando el color de fondo. En el siguiente ejemplo vamos a dibujar primero un rectángulo de 300px x 300px en la posición (100px, 100px). Seguidamente vamos a eliminar un área en su interior usando la herramienta clearRect:

contexto.fillRect(100, 100, 300, 300);
contexto.clearRect(150, 150, 200, 200);

Este será el resultado:

Cómo agregar texto

Existen una serie de métodos que te permitirán agregar y personalizar textos. Vamos a ver cada uno de ellos a continuación junto con algunos ejemplos de uso.

fillText: dibuja un texto

El método fillText(texto, x, y, maxAncho) permite agregar el texto definido mediante el parámetro texto en la posición (x, y). También es posible configurar el ancho máximo del texto mediante el parámetro maxAncho, que acepta un valor el pixels:

const texto = "Es toda una experiencia vivir con miedo, eso es lo que significa ser esclavo";
contexto.fillText(texto, 10, 90);

Este es el resultado.

font: tipo de fuente

La propiedad font permite configurar el tipo de fuente del texto, cuyo valor por defecto es 10px sans-serif. Es posible establecer el estilo de la fuente, la variante, el peso, el tamaño y el tipo de fuente.

  • Se aceptan los estilos: normal, italic, oblique.
  • Se aceptan las variantes: normal, small-caps.
  • En cuanto al peso, puede ser uno de los siguientes valores o tipos: normal, bold, bolder, lighter, 100, 200, 300, 400, 500, 600, 700, 800, 900.

A continuación puedes ver un ejemplo.

const texto = "Es toda una experiencia vivir con miedo";
contexto.font="italic small-caps bold 30px arial";
contexto.fillText(texto, 10, 90);

Ese sería el resultado.

strokeText: dibuja un texto sin relleno

El método strokeText(texto, x, y, maxAncho) se usa para agregar un texto sin relleno. Acepta una cadena de texto mediante el parámetro texto y la posición (x, y) del mismo. También se puede configurar el ancho máximo del texto mediante el parámetro maxAncho, que acepta un valor el pixels:

A continuación puedes ver un ejemplo.

const texto = "Mas humanos que los humanos.";
contexto.font="small-caps bold 24px arial";
contexto.strokeText(texto, 10, 90);

Ese sería el resultado:

textAlign: alineación del texto

La propiedad textAlign permite alinear el texto cuando se usan los métodos fillText() o strokeText(), aceptando uno de los siguientes valores:

  • start: El texto comenzará en la posición indicada. Es el valor por defecto.
  • end: El texto finalizará en la posición indicada.
  • center: El centro del texto estará en la posición indicada.
  • left: El texto comenzará en la posición indicada.
  • right: El texto finalizará en la posición indicada.

A continuación puedes ver un ejemplo:

contexto.strokeStyle = "red";
contexto.moveTo(350, 20);
contexto.lineTo(350, 370);
contexto.stroke();

contexto.textAlign = "start";
contexto.fillText("textAlign=start", 350, 120);
contexto.textAlign = "end";
contexto.fillText("textAlign=end", 350, 160);
contexto.textAlign = "left";
contexto.fillText("textAlign=left", 350, 200);
contexto.textAlign = "center";
contexto.fillText("textAlign=center", 350, 240);
contexto.textAlign = "right";
contexto.fillText("textAlign=right", 350, 280);

Este sería el resultado.

textBaseline: alineación vertical del texto

La propiedad textBaseline permite alinear el texto verticalmente, ajustando la línea base, cuando se usan los métodos fillText() o strokeText(), aceptando uno de los siguientes valores: alphabetic (defecto), top, hanging, middle, ideographic, bottom. Estos valores toman las características del texto como punto de referencia:

A continuación puedes ver un ejemplo:

contexto.beginPath();
contexto.moveTo(20,50);
contexto.lineTo(200,50);
contexto.stroke();
contexto.textBaseline='alphabetic';
contexto.font='30px Arial';
contexto.fillText('Hey, colega!',20,50);;

contexto.beginPath();
contexto.moveTo(20,100);
contexto.lineTo(200,100);
contexto.stroke();
contexto.textBaseline='bottom';
contexto.font='30px Arial';
contexto.fillText('Hey, colega!',20,100);

contexto.beginPath();
contexto.moveTo(20,125);
contexto.lineTo(200,125);
contexto.stroke();
contexto.textBaseline='hanging';
contexto.font='30px Arial';
contexto.fillText('Hey, colega!',20,125);;

contexto.beginPath();
contexto.moveTo(20,210);
contexto.lineTo(200,210);
contexto.stroke();
contexto.textBaseline='ideographic';
contexto.font='30px Arial';
contexto.fillText('Hey, colega!',20,210);;

contexto.beginPath();
contexto.moveTo(20,255);
contexto.lineTo(200,255);
contexto.stroke();
contexto.textBaseline='middle';
contexto.font='30px Arial';
contexto.fillText('Hey, colega!',20,255);;

contexto.beginPath();
contexto.moveTo(20,300);
contexto.lineTo(200,300);
contexto.stroke();
contexto.textBaseline='top';
contexto.font='30px Arial';
contexto.fillText('Hey, colega!',20,300);

Este es el resultado del código anterior:

measureText: Anchura de un texto

El método measureText(texto) se utiliza para obtener el ancho del texto que acepta como parámetro:

const texto = 'Este texto tiene una anchura de ';
contexto.font = 'small-caps bold 24px arial';

const anchura = contexto.measureText(texto).width;
contexto.fillText(texto + ' ' + anchura + 'px'  , 10, 90);

Este sería el resultado:

Cómo cambiar colores y estilos

A continuación vamos a ver una serie de métodos mediante los cuales podrás cambiar el color de relleno de los diferentes elementos del canvas, además de poder establecer un gradiente o un patrón.

fillStyle: color de relleno

Para cambiar el color de relleno debes usar un color con el método fillStyle(color). Su valor por defecto es #000000. En el siguiente ejemplo seleccionamos el color rojo y luego dibujamos un cuadrado, cuyo color de relleno será el color que hemos indicado:

contexto.fillStyle = "#d91212";
contexto.fillRect(50, 50, 150, 150);

Este sería el resultado:

strokeStyle: color del trazo

Para cambiar el color del trazado debes usar el método strokeStyle. En el siguiente ejemplo seleccionamos un color y luego dibujamos un cuadrado cuyo borde será del color seleccionado:

contexto.lineWidth=20;
contexto.strokeStyle = "#ff0000";
contexto.strokeRect(50, 50, 150, 150);

Este sería el resultado:

createLinearGradient: gradiente de relleno lineal

El método createLinearGradient(x1, x1, y2, y2) permite crear un gradiente lineal que va desde las coordenadas (x1, y1) hasta las coordenadas (x2, y2).

Mediante el método addColorStop(stop, color) podemos modificar los colores del gradiente. El parámetro stop del método addColorStop acepta un valor entre 0.0 y 1.0, que es la posición en la que se producirá el cambio al color especificado mediante el parámetro color.

Para aplicar el gradiente creado, debes usar los métodos fillStyle(gradiente), strokeStyle(gradiente) o strokeText(gradiente) con el gradiente como parámetro. A continuación crearemos un gradiente lineal que va de izquierda a derecha con los colores negro y blanco:

const contexto = miCanvas.getContext('2d');

const gradiente = contexto.createLinearGradient(0, 0, 160, 0);
gradiente.addColorStop(0, "#000");
gradiente.addColorStop(1, "#fff");

contexto.fillStyle = gradiente;
contexto.fillRect(50, 50, 150, 150);

Este sería el resultado:

A continuación crearemos un gradiente lineal que va de izquierda a derecha con varios colores:

const gradiente = contexto.createLinearGradient(0, 0, 160, 0);
gradiente.addColorStop(0, "black");
gradiente.addColorStop(0.3, "pink");
gradiente.addColorStop(0.5, "blue");
gradiente.addColorStop(0.6, "green");
gradiente.addColorStop(0.8, "yellow");
gradiente.addColorStop(1, "red");

contexto.fillStyle = gradiente;
contexto.fillRect(50, 50, 150, 150);

Este sería el resultado:

También es posible aplicar un gradiente a un borde:

const gradiente = contexto.createLinearGradient(50, 50, 180, 0);

gradiente.addColorStop("0", "green");
gradiente.addColorStop("0.5" ,"blue");
gradiente.addColorStop("1.0", "yellow");

contexto.strokeStyle = gradiente;
contexto.lineWidth = 20;
contexto.strokeRect(50, 50, 150, 150);

Este sería el resultado:

También es posible aplicar un gradiente a un texto:

const gradiente = contexto.createLinearGradient(50, 50, 180, 0);

gradiente.addColorStop("0", "green");
gradiente.addColorStop("0.5" ,"blue");
gradiente.addColorStop("1.0", "red");

contexto.lineWidth=2;
contexto.strokeStyle = gradiente;
contexto.font = "30px Arial";
contexto.strokeText("Que pasa, colega!", 10, 50);

Este sería el resultado:

createRadialGradient: gradiente de relleno radial

El método createRadialGradient(x1, x1, r1, y2, y2, r2) permite crear un gradiente radial que va desde las coordenadas (x1, y1) hasta las coordenadas (x2, y2). El parámetro r0 define el radio del círculo inicial y el parámetro r1 define el radio del círculo final.

Mediante el método addColorStop(stop, color) podemos modificar los colores del gradiente. El parámetro stop del método addColorStop acepta un valor entre 0.0 y 1.0, que es la posición en la que se producirá el cambio al color especificado mediante el parámetro color.

Para aplicar el gradiente creado, debes usar el método fillStyle(gradiente) con el gradiente como parámetro. A continuación crearemos un rectángulo cuyo color cambiará circularmente desde su centro hacia el exterior:

const contexto = miCanvas.getContext('2d');

const gradiente = contexto.createRadialGradient(75, 50, 5, 90, 60, 100);
gradiente.addColorStop(0, "red");
gradiente.addColorStop(1, "yellow");

contexto.fillStyle = gradiente;
contexto.fillRect(10, 10, 150, 100);

Este sería el resultado:

createPattern: patrón

El método createPattern(image, repeat) permite crear un patrón que estará rellenado con la imagen, vídeo o canvas especificado mediante el parámetro imagen. Mediante el parámetro repeat podemos especificar si el patrón se repetirá en diferentes direcciones, aceptando los siguientes valores:

  • repeat: El patrón se repetirá tanto en horizontal como en vertical.
  • reapeat-x: El patrón se repetirá en horizontal.
  • repeat-y: El patrón se repetirá en vertical.
  • no-repeat: El patrón no se repetirá.

Para aplicar el gradiente creado, debes usar el método fillStyle(patron) con el patron como parámetro. A continuación crearemos un rectángulo cuyo patrón se repetirá tanto en horizontal como en vertical:

const img = new Image();
img.src = 'mini-luna.jpg';

img.onload = function(){
  const patron = contexto.createPattern(img,'repeat');
  contexto.rect(50, 50, 200, 200);
  contexto.fillStyle = patron;
  contexto.fill();
}

Este sería el resultado:

Cómo agregar sombras

A continuación vamos a ver las propiedades que permiten agregar y personalizar sombras de los diferentes elementos.

shadowBlur: agrega una sombra

La propiedad shadowBlur configura lo borrosa que será la sombra que se pretende agregar. La sombra será negra por defecto:

contexto.shadowBlur = 20;
contexto.shadowColor = "black";
contexto.fillStyle = "red";
contexto.fillRect(20, 20, 100, 80);

Este será el resultado:

shadowColor: color de la sombra

La propiedad shadowColor se usa para definir el color de la sombra que se pretende agregar. El color de la sombra por defecto será el negro:

contexto.shadowBlur = 20;
contexto.shadowColor = "#ba00ff";
contexto.fillStyle = "red";
contexto.fillRect(50, 50, 100, 100);

Este será el resultado:

shadowOffsetX: distancia horizontal

La propiedad shadowOffsetX se usa para definir la distancia en horizontal desde el elemento al que se agrega la sombra hasta la sombra en sí misma:

contexto.shadowBlur = 10;
contexto.shadowColor = "#ba00ff";
contexto.shadowOffsetX = 20;
contexto.fillStyle = "red";
contexto.fillRect(50, 50, 100, 100);

Este será el resultado:

shadowOffsetY: distancia vertical

La propiedad shadowOffsetY se usa para definir la distancia en vertical desde el elemento al que se agrega la sombra hasta la sombra en sí misma:

contexto.shadowBlur = 10;
contexto.shadowColor = "#ba00ff";
contexto.shadowOffsetY = 20;
contexto.fillStyle = "red";
contexto.fillRect(50, 50, 100, 100);

Este será el resultado:

Cómo agregar imágenes

Para agregar imágenes, vídeos e incluso otros canvas puedes usar el método drawImage(img, x, y, ancho, alto). El parámetro img representa la imagen, el vídeo o el canvas a agregar, mientras que las coordenadas (x, y) reprensentan su posición en el canvas. En caso de querer modificar la anchura y la altura del elemento agregado, puedes usar los parámetros opcionales ancho y alto para definir su anchura y su altura respectivamente.

Además, existe una segunda sintaxis de este método que acepta las coordenadas (sx, yx), que permiten recortar las imágenes en el punto indicado, pudiendo definir también el ancho y el alto de la imagen recortada mediante los parámetros sancho y salto: drawImage(img, sx, sy, sancho, salto, x, y, ancho, alto).

A continuación vamos a ver cómo agregar varios elementos con este método.

drawImage: imágenes

Para dibujar una imagen, primero debemos crear una imagen vacía y luego cargarla estableciendo su origen. Seguidamente, tendremos que esperar a que esté cargada antes de poder mostrarla en el canvas:

const img = new Image();
img.src = 'luna.jpg';

img.onload = function(){
  contexto.drawImage(img, 50, 50);
}

Este sería el resultado:

Ahora vamos a agregar la misma imagen pero redimensionándola con parámetros adicionales:

const img = new Image();
img.src = 'luna.jpg';

window.onload = function(){
  contexto.drawImage(img, 50, 10, 150, 123);
}

Este sería el resultado:

Y finalmente, vamos a recortar la imagen, mostrando solamente una porción de la misma:

const img = new Image();
img.src = 'luna.jpg';

window.onload = function(){
  contexto.drawImage(img, 50, 100, 100, 75, 75, 10, 150, 123);
}

Este sería el resultado, en donde tal y como puedes ver, se ha aplicado un zoom, ya que la imagen ocupa el mismo espacio que en el ejemplo anterior:

drawImage: vídeos

Para dibujar un vídeo, primero debemos agregar un vídeo al documento y establecer su origen. Cuando reproduzcamos el vídeo, dibujarmos las imágenes del vídeo en el canvas cada 10 milisegundos:

<video id="mi-video" controls width="270" autoplay>
  <source src="conejo.mp4" type='video/mp4'>
</video>

Código JavaScript en el que también agregamos los controles de pausa y finalización del vídeo, momento en el que se dejará de refrescar el canvas.

const video = document.getElementById("mi-video");
video.addEventListener("play", function() {
	intervalo = window.setInterval(function() {
		contexto.drawImage(video, 20, 20, 260, 125)
	}, 10);
}, false);

video.addEventListener('pause',function() {
	window.clearInterval(intervalo);
},false);

video.addEventListener('ended',function() {
	clearInterval(intervalo);
},false);

Este sería el resultado:

Cómo trabajar con un canvas

Hasta ahora hemos visto ejemplos muy simples. Sin embargo, lo habitual es agregar varios elementos a la vez para generar elementos más complejos e incluso animarlos e interactuar con ellos. A continuación vamos a ver cómo hacerlo en base a varios ejemplos.

Cómo agregar varios elementos a un canvas

Vamos a dibujar varios cuadrados usando el método fillRect(). Además, usaremos un gradiente lineal como relleno que iremos modificando con cada elemento que agreguemos.

Este sería el código JavaScript:

for (let i = 0; i < 60; i++) {
  for (let j = 0; j < 60; j++) {
    contexto.fillStyle = `rgb(${i * 5}, ${j * 5}, ${(i+j) * 50})`;
    contexto.fillRect(j * 20, i * 20, 10, 10);
  }
}

Código HTML, JS y CSS completo:

See the Pen Canvas squares by Edu Lazaro (@edulazaro) on CodePen.36754

También podemos agregar un rectángulo blanco para dar sensación de transparencia y dibujar una rejilla. Este sería el código JavaScript:

for (let i = 0; i < 61; i++) {
  for (let j = 0; j < 61; j++) {
    contexto.strokeStyle = `rgb(${i * 5}, ${j * 5}, ${(i+j) * 50})`;
    contexto.strokeRect(j * 20, i * 20, 20, 20);
  }
}

Código HTML, JS y CSS completo:

See the Pen Canvas grid by Edu Lazaro (@edulazaro) on CodePen.36754

Vamos a agregar otro tipo de formas. Por ejemplo, vamos a crear un fondo con burbujas transparentes de varios colores:

const miCanvas = document.getElementById('mi-canvas');
miCanvas.width = window.innerWidth;
miCanvas.height = window.innerHeight;

const contexto = miCanvas.getContext('2d');

const circlesCount = 400
const colorArray = [
  'rgb(88, 81, 195, 0.7)',
  'rgb(116,151, 245, 0.7)',
  'rgb(160, 203, 235, 0.7)',
  'rgb(160, 203, 235, 0.7)',
  'rgb(70 170, 219, 0.7)',
  'rgb(60, 119, 198, 0.7)',
  'rgb(9, 75, 153, 0.7)'
]

const init = () => {
  for (let i = 0; i < circlesCount; i++) {
    const radio = Math.random() * 50 + 1;
    const x = Math.random() * (innerWidth - radio  * 2) + radio;
    const y = Math.random() * (innerHeight - radio  * 2) + radio;
    const dx = (Math.random() - 0.5) * 2;
    const dy = (Math.random() - 0.5) * 2;

    const bubble = new Bubble(x, y, dx, dy, radio);
    bubble.draw();
  }
}

const Bubble = function(x, y, dx, dy, radio) {
  this.x = x;
  this.y = y;
  this.dx = dx;
  this.dy = dy;
  this.radio = radio;
  this.minRadius = radio;
  this.color = colorArray[Math.floor(Math.random() * colorArray.length)];

  this.draw = function() {
    contexto.beginPath();
    contexto.arc(this.x, this.y, this.radio, 0, Math.PI * 2, false);
    contexto.strokeStyle = 'black';
    contexto.stroke();
    contexto.fillStyle = this.color;
    contexto.fill();
  }
}

init();

Código HTML, JS y CSS completo:

See the Pen Canvas bubbles by Edu Lazaro (@edulazaro) on CodePen.36754

Cómo animar elementos en un canvas

Vamos a ver un ejemplo de cómo animar elementos en un canvas. Para ello debemos agregar un bucle que refresque la imagen en pantalla cada cierto tiempo.

Vamos a agregar una serie de burbujas en movimiento que se muevan en una trayectoria fija hasta llegar al borde, momento en el que rebotarán. Para refrescar la pantalla usaremos el método requestAnimationFrame():

const miCanvas = document.getElementById('mi-canvas');
miCanvas.width = window.innerWidth;
miCanvas.height = window.innerHeight;

const contexto = miCanvas.getContext('2d');
const circleArray = [];

const circlesCount = 400
const colorArray = [
  'rgb(88, 81, 195, 0.7)',
  'rgb(116,151, 245, 0.7)',
  'rgb(160, 203, 235, 0.7)',
  'rgb(160, 203, 235, 0.7)',
  'rgb(70 170, 219, 0.7)',
  'rgb(60, 119, 198, 0.7)',
  'rgb(9, 75, 153, 0.7)'
]

const init = () => {
  circleArray.length = 0
  for (let i = 0; i < circlesCount; i++) {
    const radio = Math.random() * 20 + 1;
    const x = Math.random() * (innerWidth - radio  * 2) + radio;
    const y = Math.random() * (innerHeight - radio  * 2) + radio;
    const dx = (Math.random() - 0.5) * 2;
    const dy = (Math.random() - 0.5) * 2;

    const bubble = new Bubble(x, y, dx, dy, radio);
    circleArray.push(bubble);
  }

}

const Bubble = function(x, y, dx, dy, radio) {
  this.x = x;
  this.y = y;
  this.dx = dx;
  this.dy = dy;
  this.radio = radio;
  this.minRadius = radio;
  this.color = colorArray[Math.floor(Math.random() * colorArray.length)];

  this.draw = function() {
    contexto.beginPath();
    contexto.arc(this.x, this.y, this.radio, 0, Math.PI * 2, false);
    contexto.strokeStyle = 'black';
    contexto.stroke();
    contexto.fillStyle = this.color;
    contexto.fill();
  }
  
  this.update = function() {
    if (this.x + this.radio > innerWidth || this.x - this.radio < 0) {
      this.dx = -this.dx
    }

    if (this.y + this.radio > innerHeight || this.y - this.radio < 0) {
      this.dy = -this.dy
    }

    this.x += this.dx
    this.y += this.dy
    
    this.draw()
  }
}

const animate = () => {
  requestAnimationFrame(animate);
  contexto.clearRect(0, 0, innerWidth, innerHeight);

  for (let i = 0; i < circleArray.length; i++) {
    circleArray[i].update();
  }
}

init();
animate();

Código HTML, JS y CSS completo:

See the Pen Canvas animated bubbles by Edu Lazaro (@edulazaro) on CodePen.36754

Cómo interactuar con elementos en un canvas

Podemos interactuar con los elementos de un canvas detectando diferentes eventos con el mismo, ya sean movimientos de ratón, pulsaciones en el teclado o incluso movimiento a través de una cámara.

En el siguiente ejemplo vamos a detectar movimiento con el ratón mediante el evento mousemove. Lo que vamos a hacer es aumentar el radio de las burbujas de nuestro ejemplo anterior cuando estén carca del ratón, concratemente a una distancia menor a la definida en la variable distanciaRaton:

const miCanvas = document.getElementById('mi-canvas');
miCanvas.width = window.innerWidth;
miCanvas.height = window.innerHeight;

const contexto = miCanvas.getContext('2d');


const maxRadius = 40;
const distanciaRaton = 100;
const circlesCount = 400

const circleArray = [];
const colorArray = [
  'rgb(88, 81, 195, 0.7)',
  'rgb(116,151, 245, 0.7)',
  'rgb(160, 203, 235, 0.7)',
  'rgb(160, 203, 235, 0.7)',
  'rgb(70 170, 219, 0.7)',
  'rgb(60, 119, 198, 0.7)',
  'rgb(9, 75, 153, 0.7)'
]

let mousex = undefined;
let mousey = undefined;

const init = () => {
  circleArray.length = 0
  for (let i = 0; i < circlesCount; i++) {
    const radio = Math.random() * 20 + 1;
    const x = Math.random() * (innerWidth - radio  * 2) + radio;
    const y = Math.random() * (innerHeight - radio  * 2) + radio;
    const dx = (Math.random() - 0.5) * 2;
    const dy = (Math.random() - 0.5) * 2;

    const bubble = new Bubble(x, y, dx, dy, radio);
    circleArray.push(bubble);
  }

}

const Bubble = function(x, y, dx, dy, radio) {
  this.x = x;
  this.y = y;
  this.dx = dx;
  this.dy = dy;
  this.radio = radio;
  this.minRadius = radio;
  this.color = colorArray[Math.floor(Math.random() * colorArray.length)];

  this.draw = function() {
    contexto.beginPath();
    contexto.arc(this.x, this.y, this.radio, 0, Math.PI * 2, false);
    contexto.strokeStyle = 'black';
    contexto.stroke();
    contexto.fillStyle = this.color;
    contexto.fill();
  }
  
  this.update = function(i) {
    if (this.x + this.radio > innerWidth || this.x - this.radio < 0) {
      this.dx = -this.dx
    }

    if (this.y + this.radio > innerHeight || this.y - this.radio < 0) {
      this.dy = -this.dy
    }

    this.x += this.dx
    this.y += this.dy
    
	if (mousex - this.x < distanciaRaton && mousex - this.x > -distanciaRaton && mousey - this.y < distanciaRaton && mousey - this.y > -distanciaRaton) {
      if (this.radio < maxRadius) {
		  this.radio += 1;
	  }
    } else {
      if (this.radio > this.minRadius) this.radio -= 1;
    }
	
    this.draw()
  }
}

const animate = () => {
  requestAnimationFrame(animate);
  contexto.clearRect(0, 0, innerWidth, innerHeight);

  for (let i = 0; i < circleArray.length; i++) {
    circleArray[i].update(i);
  }
}

init();
animate();

window.addEventListener('mousemove', (e) => {
  mousex = e.x;
  mousey = e.y;
});

Código HTML, JS y CSS completo:

See the Pen Canvas animated bubbles interaction by Edu Lazaro (@edulazaro) on CodePen.36754

Cómo solucionar problemas de rendimiento

Cuando agregas demasiados elementos y además están animados, pueden surgir problemas de rendimiento. Esto se agrava cuando estos elementos son imágenes o textos.

Una posible recomendación consiste en cargar los elementos que se repitan varias veces fuera del canvas, y agregarlos al canvas primario en todas las ocasiones necesarias sin necesidad de repetir los pasos que cargan la imagen inicialmente:

miCanvas.offscreenCanvas = document.createElement('canvas');
miCanvas.offscreenCanvas.width = myCanvas.width;
miCanvas.offscreenCanvas.height = myCanvas.height;

myCanvas.getContext('2d').drawImage(myCanvas.offScreenCanvas, 0, 0);

En caso de necesitar varios tamaños de la misma imagen, es mejor cachear diferentes tamaños de las mismas en lugar de redimensionarlas al vuelo. Escalar imágenes continuamente usando drawImage() no es eficiente.

También es recomendable usar unidades enteras para los pixels, ya que los cálculos decimales como los siguientes serán menos eficientes:

contexto.drawImage(imagen, 0.3, 0.5);

Otra posible optimización consiste en usar varios canvas superpuestos, ya que no siempre será necesario refrescar todos los elementos del canvas principal:

<div id="stage">
  <canvas id="ui-layer" width="480" height="320"></canvas>
  <canvas id="game-layer" width="480" height="320"></canvas>
  <canvas id="background-layer" width="480" height="320"></canvas>
</div>
 
<style>
  #stage {
    width: 480px;
    height: 320px;
    position: relative;
    border: 2px solid black;
  }

  canvas { position: absolute; }
  #ui-layer { z-index: 3; }
  #game-layer { z-index: 2; }
  #background-layer { z-index: 1; }
</style>

Siempre es mejor escalar el canvas usando transformaciones CSS:

let escalaX = window.innerWidth / canvas.width;
let escalaY = window.innerHeight / canvas.height;

let escala = Math.min(escalaX, escalaY);

stage.style.transformOrigin = '0 0';
stage.style.transform = 'scale(' + escala + ')';

Finalmente, también es recomendable evitar la transparencia del canvas al seleccionar el contexto:

var contexto = canvas.getContext('2d', { alpha: false });

Y esto ha sido todo.


Avatar de Edu Lazaro

Edu Lázaro: Ingeniero técnico en informática, actualmente trabajo como desarrollador web y programador de videojuegos.

👋 Hola! Soy Edu, me encanta crear cosas y he redactado esta guía. Si te ha resultado útil, el mayor favor que me podrías hacer es el de compatirla en Twitter 😊

Si quieres conocer mis proyectos, sígueme en Twitter.

2 comentarios en “Tutorial de Canvas HTML: Qué es y cómo se utiliza

Deja una respuesta

“- Hey, Doc. No tenemos suficiente carretera para ir a 140/h km. - ¿Carretera? A donde vamos, no necesitaremos carreteras.”