La etiqueta HTML <canvas>

Primeros pasos a la programación gráfica con Javascript


En HTML, existe un elemento HTML llamado <canvas> que sirve como lienzo en blanco donde se puede dibujar mediante programación. Este sistema es el que se utiliza para animaciones, interacciones con el usuario o juegos basados en web.

Aunque se trata de un elemento HTML, toda la lógica de programación se realiza mediante Javascript, utilizando su propia API. Más adelante explicaremos Phaser, una librería que utiliza <canvas> por debajo para permitirnos hacer cosas más grandes de una forma más cómoda y rápida, pero es interesante tener una base de como funciona <canvas>, o si queremos hacer cosas pequeñas que no utilicen dependencias de terceros.

Si tu objetivo es trabajar con Phaser, puedes saltarte toda esta categoría de <canvas> y acceder directamente a ¿Qué es Phaser?. Generalmente, canvas te interesará si quieres no utilizar librerías externas o hacer cosas más pequeñas y ligeras, sin dependencias.

El elemento <canvas>

Para comenzar a utilizar el elemento <canvas>, básicamente lo creamos en el HTML y lo localizamos desde Javascript mediante el DOM. También es posible crearlo desde Javascript mediante document.createElement() y añadirlo al HTML si lo preferimos.

Los primeros pasos suelen ser indicar el tamaño que tendrá el lienzo, para poder verlo en la página. Eso lo haremos simplemente dándole un width y height a nuestro elemento <canvas>:

<canvas></canvas>

<script type="module">
  const canvas = document.querySelector("canvas");
  canvas.width = 320;
  canvas.height = 240;
  canvas.style.background = "#000";
</script>

Ten en cuenta que en la última línea le estamos dando un color de fondo negro al canvas mediante CSS. Esto lo estamos haciendo ahora de esta forma porque es sencillo y rápido hacerlo mediante JS/CSS, pero generalmente se utiliza el fill() o fillRect() de canvas, que veremos un poco más adelante.

El contexto del canvas

Para trabajar con canvas, tenemos que crear un contexto, que es el objeto que nos permite controlar nuestro lienzo. Este objeto se toma del canvas mediante el método .getContext() y hay que indicarle por parámetro el tipo de lienzo que queremos:

<canvas></canvas>

<script type="module">
  const canvas = document.querySelector("canvas");
  const ctx = canvas.getContext("2d");
  canvas.width = 320;
  canvas.height = 240;
  canvas.style.background = "#ccc";
</script>

Podemos establecer diferentes tipos diferentes de lienzo:

TipoObjetoOrientación del contexto
"2d"CanvasRenderingContext2DGráficos 2D (lineas, formas, texto, imágenes...).
"webgl2"WebGL2RenderingContextAPI basada en OpenGL ES 3.0 para 2D/3D con aceleración por hardware.
"webgpu"GPUCanvasContextGráficos de alta eficiencia (optimizado para tarjetas gráficas modernas).
"bitmaprenderer"ImageBitmapRenderingContextOrientado para renderizar imágenes con alto rendimiento.

En esta documentación nos vamos a centrar en gráficos 2D, que son los más sencillos para comenzar. Sin embargo, también se pueden establecer gráficos 3D utilizando WebGL2 o WebGPU, aunque son mucho más complejos.

Dibujar en el canvas

Una vez hemos creado nuestro contexto, que hemos almacenado en una variable llamada ctx, vamos a trabajar con varios métodos para dibujar en el canvas.

Antes de nada, ten en cuenta que vamos a utilizar propiedades como .fillStyle o .strokeStyle para establecer estilo y dibujar rellenos o trazos, como veremos en los ejemplos. La tabla que ves a continuación son algunas de las siguientes funciones que tenemos disponibles para utilizar con nuestro contexto 2D de canvas:

MétodoDescripción
Formas geométricas
.beginPath() / .closePath()Comienza o cierra una ruta.
.ellipse()Crea un círculo ovalado (elipse) indicando centro, radios, ángulos, etc.
.rect()Crea un rectángulo con un ancho y altura indicado.
.stroke() / .fill()Dibuja el contorno de la ruta, y rellena el interior.
.moveTo() / .lineTo()Se mueve a una coordenada sin dibujar trazo o dibujándolo.
Dibujo directo
.strokeRect() / .fillRect()Dibuja el contorno o el relleno de un rectángulo.
.strokeText() / .fillText()Dibuja el contorno o el relleno de un texto.
.roundRect()Dibuja un rectángulo con bordes redondeados.
.clearRect()Borra el lienzo completo o una porción de él.
Curvas
.arc() / .arcTo()Dibuja el arco de un círculo o el arco entre dos líneas.
.bezierCurveTo()Dibuja una curva bézier cúbica a partir de un punto con 3 puntos de control.
.quadraticCurveTo()Dibuja una curva cuadrática a partir de un punto, con 2 puntos de control.

Echemos un vistazo a algunos ejemplos sencillos para comprender su uso:

Dibujar con relleno

Para empezar, quizás lo más sencillo sería dibujar pequeñas formas geométricas con relleno. Para ello, lo más fácil es utilizar el método .fillRect() (más directo) o el método .fill() (menos directo, pero ideal para dibujos complejos de varias partes):

<canvas></canvas>

<script type="module">
  const canvas = document.querySelector("canvas");
  const ctx = canvas.getContext("2d");
  canvas.width = 200;
  canvas.height = 200;
  canvas.style.background = "#ccc";    // lightgrey

  // Dibuja rectángulo 100x100 en (50,50) de color indigo
  ctx.fillStyle = "indigo";
  ctx.fillRect(25, 25, 100, 100);

  // Dibuja un rectángulo y una elipse (formas compuestas) de color rosa
  ctx.fillStyle = "deeppink";
  ctx.rect(50, 50, 100, 100);
  ctx.ellipse(125, 125, 50, 50, Math.PI / 3, 0, 2 * Math.PI);
  ctx.fill();
</script>
  • 1️⃣ Primero, con .fillStyle establecemos el color de relleno con el que vamos a pintar.
  • 2️⃣ Luego, con .fillRect(x, y, width, height) dibujamos un rectángulo.
  • 3️⃣ En este caso, el rectángulo se dibuja automáticamente.
  • 4️⃣ Sin embargo, en el segundo ejemplo, dibujamos un rectángulo con rect() y un círculo con ellipse(), pero estas no se dibujan directamente y hay que llamar a fill() para hacerlo.

El primer grupo dibuja un rectángulo de color morado y luego, en el segundo grupo, se dibuja un rectángulo y un círculo rosa -ambos fusionados- sobre el anterior.

Dibujar trazos

Si no buscamos dibujar formas rellenas, sino trazos, podemos hacer lo mismo, pero haciendo uso de stroke en lugar de fill. Observa que mediante .lineWidth establecemos el número de píxeles que tendrá el trazo. Con .strokeStyle definimos el color.

<canvas></canvas>

<script type="module">
  const canvas = document.querySelector("canvas");
  const ctx = canvas.getContext("2d");
  canvas.width = 200;
  canvas.height = 200;
  canvas.style.background = "#ccc";    // lightgrey

  ctx.lineWidth = 4;

  // Dibuja rectángulo 100x100 en (50,50) de color indigo
  ctx.strokeStyle = "indigo";
  ctx.strokeRect(25, 25, 100, 100);

  // Dibuja un rectángulo y una elipse (formas compuestas) de color rosa
  ctx.strokeStyle = "deeppink";
  ctx.rect(50, 50, 100, 100);
  ctx.stroke();

  ctx.beginPath();
  ctx.ellipse(125, 125, 50, 50, Math.PI / 3, 0, 2 * Math.PI);
  ctx.stroke();
  ctx.closePath();
</script>

Observa que en este caso, el código ha cambiado un poco porque, salvo que utilices .beginPath() y .closePath(), con los trazos se dibuja como si no soltaras el lapiz de la hoja. En nuestro caso, hemos usado las dos funciones anteriores, para indicar que vamos a empezar otro trazo diferente, y que no debe añadir esa linea que conectaría el rectángulo con el círculo:

Prueba a eliminar las líneas de los métodos .beginPath() y .closePath() y verás como se genera el dibujo.

Dibujar con líneas

Si no nos interesan formas geométricas básicas, sino que queremos realizar una o múltiples líneas dibujadas de forma personalizada, podemos utilizar los métodos .moveTo() y .lineTo().

Con ellos podemos realizar trazos moviéndonos a las coordenadas (x, y) indicadas, y levantando el lapiz (sin dibujar) cuando usamos .moveTo() y dibujando cuando usamos .lineTo(x, y).

<canvas></canvas>

<script type="module">
  const canvas = document.querySelector("canvas");
  const ctx = canvas.getContext("2d");
  canvas.width = 200;
  canvas.height = 200;
  canvas.style.background = "#ccc";    // lightgrey

  ctx.lineWidth = 4;
  ctx.strokeStyle = "red";
  ctx.beginPath();
  ctx.moveTo(100, 100);
  ctx.lineTo(150, 150);
  ctx.lineTo(50, 150);
  ctx.lineTo(50, 50);
  ctx.lineTo(150, 50);
  ctx.stroke();
  ctx.closePath();
</script>

Si quieres experimentar un poco, prueba a mover el closePath() de línea, y moverlo antes del .stroke(). Comprobarás que en ese caso, en lugar de dibujar y luego cerrar el camino, cerrará el camino y luego lo dibujará.

Dibujar textos

Por último, y no por ello menos importante, con .font podemos establecer detalles relacionados con la tipografía, como el tamaño o el nombre. Luego, con fillStyle establecemos el color de texto.

Finalmente, con .fillText() podemos establecer un texto en unas coordenadas concretas:

<canvas></canvas>

<script type="module">
  const canvas = document.querySelector("canvas");
  const ctx = canvas.getContext("2d");
  canvas.width = 640;
  canvas.height = 480;

  ctx.fillStyle = "blue";
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = "white";
  ctx.font = '64px EnterCommand';
  ctx.fillText("PLAY ▶", 75, 100);
  ctx.fillText("--:--", 475, 100);
  ctx.fillText("MANZ.DEV", 75, 400);
  ctx.fillText("0:00:00", 425, 400);
</script>

En este ejemplo, se simula una antigua pantalla de TV.

Estado del canvas

Debes saber que nuestro <canvas> tiene la posibilidad de guardar su estado en una estructura de tipo pila y recuperarlo posteriormente. Cuando hablamos de estado nos referimos a propiedades como .fillStyle, .strokeStyle, .globalAlpha, etc... También transformaciones como translate, scale, rotate o recortes con .clip().

Utilizando el método .save() guardamos en la pila, mientras que con .restore() recuperamos el último estado guardado.

Observa el siguiente ejemplo, donde:

  • 1️⃣ Establecemos un color rosa de relleno y guardamos el estado.
  • 2️⃣ Cambiamos el color de relleno a verde.
  • 3️⃣ Cada segundo, dibujamos un cuadradito (verde).
  • 4️⃣ Cuando hagamos click, recuperamos el estado y lo pintará rosa.
<canvas></canvas>

<script type="module">
  const canvas = document.querySelector("canvas");
  const ctx = canvas.getContext("2d");
  canvas.width = 200;
  canvas.height = 200;
  canvas.style.background = "#ccc";    // lightgrey

  // Guardamos el estado con el color de relleno rosa
  ctx.fillStyle = "deeppink";
  ctx.save();

  // Cada segundo se añadirá un nuevo cuadrado verde
  ctx.fillStyle = "green";
  setInterval(() => {
    const x = Math.floor(Math.random() * canvas.width);
    const y = Math.floor(Math.random() * canvas.height);
    ctx.fillRect(x, y, 50, 50);
  }, 1000);

  // Cuando hagas click, el color se restaura a rosa
  canvas.addEventListener("click", () => ctx.restore());
</script>

Además de estos métodos para manejar el estado, también podemos realizar transformaciones o acciones similares. Estos son los métodos que tenemos en canvas para realizarlos:

MétodoDescripción
Estado
.save()Guarda el estado actual del lienzo en una pila.
.restore()Restaura el último estado guardado en la pila con .save().
.reset()Reinicia el estado del lienzo. Experimental, se recomienda usar .resetTransform().
Transformaciones
.transform()Aplica una transformación acumulativa, respetando la transformación previa.
.setTransform()Aplica una transformación, reemplazando cualquier transformación previa.
.getTransform()Devuelve un objeto DOMMatrix con la transformación actual del lienzo.
.resetTransform()Elimina todas las transformaciones previas. Igual a .setTransform(1, 0, 0, 1, 0, 0).
.scale()Escala según los factores proporcionados.
.translate()Desplaza a un nuevo punto definido por (x, y).
.rotate()Rota en torno a su origen actual (ángulo en radianes).
.clip()Recorta una región particular del lienzo.

Los métodos de transformación funcionan de forma muy similar a las transformaciones de CSS. Ten cuidado al utilizarlos, ya que actuan sobre todo el lienzo.

¿Quién soy yo?

Soy Manz, vivo en Tenerife (España) y soy streamer partner en Twitch y profesor. Me apasiona el universo de la programación web, el diseño y desarrollo web y la tecnología en general. Aunque soy full-stack, mi pasión es el front-end, la terminal y crear cosas divertidas y locas.

Puedes encontrar más sobre mi en Manz.dev