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:
Tipo | Objeto | Orientación del contexto |
---|---|---|
"2d" | CanvasRenderingContext2D | Gráficos 2D (lineas, formas, texto, imágenes...). |
"webgl2" | WebGL2RenderingContext | API basada en OpenGL ES 3.0 para 2D/3D con aceleración por hardware. |
"webgpu" | GPUCanvasContext | Gráficos de alta eficiencia (optimizado para tarjetas gráficas modernas). |
"bitmaprenderer" | ImageBitmapRenderingContext | Orientado 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étodo | Descripció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 conellipse()
, pero estas no se dibujan directamente y hay que llamar afill()
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étodo | Descripció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.