El elemento <canvas> es un lienzo de píxeles sobre el que JavaScript dibuja. Con él se hacen gráficas, juegos, editores de imágenes y simulaciones. Esta página cubre lo básico para empezar a dibujar.
También te puede interesar
Formas, líneas y texto
Para probarlo, guarda el código en un archivo .html, ábrelo en el navegador y abre la consola con F12 (en Mac, Cmd+Opción+J). Todos los ejemplos de esta página son un único archivo, sin nada más que instalar.
<!doctype html>
<html lang="es">
<head><meta charset="utf-8"><title>Canvas</title></head>
<body>
<canvas id="lienzo" width="400" height="250"></canvas>
<script type="module">
const lienzo = document.querySelector("#lienzo");
const ctx = lienzo.getContext("2d");
// Fondo
ctx.fillStyle = "#f5f8fb";
ctx.fillRect(0, 0, lienzo.width, lienzo.height);
// Un rectángulo relleno y otro solo con borde
ctx.fillStyle = "#1e64af";
ctx.fillRect(20, 20, 120, 80);
ctx.strokeStyle = "#00264f";
ctx.lineWidth = 4;
ctx.strokeRect(160, 20, 120, 80);
// Un círculo: un arco de 0 a 2π
ctx.beginPath();
ctx.arc(340, 60, 40, 0, Math.PI * 2);
ctx.fillStyle = "#33475b";
ctx.fill();
// Una línea quebrada
ctx.beginPath();
ctx.moveTo(20, 200);
ctx.lineTo(120, 140);
ctx.lineTo(220, 200);
ctx.lineTo(320, 140);
ctx.strokeStyle = "#1e64af";
ctx.lineWidth = 3;
ctx.stroke();
// Texto
ctx.fillStyle = "#0f1f33";
ctx.font = "bold 20px sans-serif";
ctx.fillText("Hola, canvas", 20, 240);
// Leer un píxel: rojo, verde, azul y opacidad
console.log("píxel (50, 50):", ctx.getImageData(50, 50, 1, 1).data.join(", "));
console.log("píxel (340, 60):", ctx.getImageData(340, 60, 1, 1).data.join(", "));
console.log("píxel (390, 240):", ctx.getImageData(390, 240, 1, 1).data.join(", "));
</script>
</body>
</html>consolapíxel (50, 50): 30, 100, 175, 255 píxel (340, 60): 51, 71, 91, 255 píxel (390, 240): 245, 248, 251, 255
Cómo funciona
Todo se hace sobre el contexto. getContext("2d") devuelve el objeto con los métodos de dibujo. El lienzo en sí solo guarda los píxeles.
Las coordenadas empiezan arriba a la izquierda, y la y crece hacia abajo, al revés que en matemáticas. Es la causa número uno de dibujos boca abajo.
Primero se elige el estilo, luego se dibuja. fillStyle, strokeStyle, lineWidth y font afectan a todo lo que se dibuje a continuación, hasta que los cambies.
Rectángulos directos, todo lo demás con caminos. fillRect y strokeRect dibujan al momento. Para círculos, líneas o polígonos se abre un camino con beginPath(), se traza con moveTo, lineTo o arc, y se pinta con fill() o stroke(). Los ángulos de arc van en radianes: una vuelta entera es Math.PI * 2.
Y getImageData lee los píxeles: cuatro números del 0 al 255 por píxel, rojo, verde, azul y opacidad. En la salida se ve el azul del rectángulo, el gris del círculo y el fondo. Chromium avisa de que, si vas a leer muchas veces, conviene crear el contexto con getContext("2d", { willReadFrequently: true }):
aviso en la consola de ChromiumCanvas2D: Multiple readback operations using getImageData are faster with the willReadFrequently attribute set to true. See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently
El error frecuente: un canvas borroso
Si el tamaño se fija con CSS en lugar de con los atributos width y height, el canvas se estira y se ve borroso. Y aunque lo hagas bien, en una pantalla de alta densidad, como la de un móvil o un Mac, cada píxel CSS son dos o tres píxeles físicos. La solución es dar al lienzo más píxeles reales y escalar el dibujo:
<!doctype html>
<html lang="es">
<head><meta charset="utf-8"><title>Canvas nítido</title></head>
<body>
<canvas id="lienzo" style="width: 400px; height: 250px"></canvas>
<script type="module">
const lienzo = document.querySelector("#lienzo");
const ctx = lienzo.getContext("2d");
const escala = window.devicePixelRatio || 1;
lienzo.width = 400 * escala;
lienzo.height = 250 * escala;
ctx.scale(escala, escala);
console.log("devicePixelRatio:", escala, "| píxeles reales:", lienzo.width, "x", lienzo.height);
ctx.fillStyle = "#1e64af";
ctx.fillRect(20, 20, 120, 80);
</script>
</body>
</html>consola, en una pantalla doble y en una normaldevicePixelRatio: 2 | píxeles reales: 800 x 500 devicePixelRatio: 1 | píxeles reales: 400 x 250
En la pantalla doble, el lienzo mide 400 x 250 en la página pero tiene 800 x 500 píxeles reales. Gracias a ctx.scale, el resto del código sigue dibujando con las coordenadas de siempre.
Animar: requestAnimationFrame
<!doctype html>
<html lang="es">
<head><meta charset="utf-8"><title>Animación</title></head>
<body>
<canvas id="lienzo" width="400" height="120"></canvas>
<script type="module">
const lienzo = document.querySelector("#lienzo");
const ctx = lienzo.getContext("2d");
let x = 30;
let velocidad = 120; // píxeles por segundo
let anterior = performance.now();
let cuadros = 0;
function cuadro(ahora) {
const dt = (ahora - anterior) / 1000;
anterior = ahora;
x += velocidad * dt;
if (x > lienzo.width - 30 || x < 30) velocidad = -velocidad;
ctx.clearRect(0, 0, lienzo.width, lienzo.height);
ctx.beginPath();
ctx.arc(x, 60, 20, 0, Math.PI * 2);
ctx.fillStyle = "#1e64af";
ctx.fill();
cuadros++;
requestAnimationFrame(cuadro);
}
requestAnimationFrame(cuadro);
setTimeout(() => console.log(`en 1 s: ${cuadros} cuadros, la bola va por x = ${Math.round(x)}`), 1000);
</script>
</body>
</html>consolaen 1 s: 60 cuadros, la bola va por x = 149
requestAnimationFrame pide al navegador que ejecute la función antes de pintar el siguiente cuadro, normalmente 60 veces por segundo, que es lo que medimos. Cada cuadro se borra el lienzo y se vuelve a dibujar todo. La clave es mover las cosas según el tiempo transcurrido (dt) y no un número fijo de píxeles por cuadro: así la bola va igual de rápido en una pantalla de 60 Hz que en una de 120. Además, el navegador pausa la animación cuando la pestaña no se ve, cosa que setInterval no hace.
Cada página de esta sección se abrió en Chromium 131, el motor de Chrome, controlado con Playwright, antes de publicarla; lo que aparece como consola es lo que imprimió de verdad. Documentación oficial: el tutorial de Canvas en MDN.