Saltar al contenido

El elemento canvas y getContext

Montar un canvas correctamente desde cero: el elemento, sus dos tamaños, la obtención del contexto y las reglas del ciclo de vida que nadie documenta.

El elemento y sus dos tamaños

El marcado mínimo es este:

HTML
<canvas id="lienzo" width="600" height="400">  Tu navegador no dibuja este gráfico. Aquí va la descripción alternativa.</canvas>

Los atributos width y height no son CSS. Son las dimensiones del búfer de píxeles, en píxeles del búfer, y su valor por defecto es 300 por 150 si no los pones. El tamaño con el que el elemento se muestra en la página lo decide CSS, exactamente igual que con una <img>, y si no pones ninguna regla el elemento se muestra a su tamaño intrínseco, que es el del búfer.

Esa dualidad es la fuente del problema más común del canvas, y merece toda una lección aparte, pero el hecho básico hay que fijarlo ya: son dos tamaños independientes, y cuando no coinciden el navegador escala el búfer para rellenar la caja, con la misma interpolación que aplicaría a una imagen.

CSS
/* Esto NO cambia el numero de pixeles del buffer: lo estira */#lienzo { width: 1200px; height: 800px; }

El contenido que pones dentro del elemento es el contenido de reserva. No se pinta nunca en un navegador que soporte canvas —todos lo hacen—, pero se expone al árbol de accesibilidad y aparece en algunas conversiones a texto. No lo dejes vacío.

getContext y la regla de exclusividad

El elemento no dibuja nada por sí mismo. Todo el dibujo pasa por un objeto de contexto:

JS
const canvas = document.getElementById('lienzo');const ctx = canvas.getContext('2d');

getContext tiene tres comportamientos que hay que conocer.

Devuelve siempre el mismo objeto. Llamarlo cien veces con "2d" sobre el mismo canvas devuelve exactamente la misma instancia. No hay coste en volver a pedirlo, aunque tampoco hay razón para hacerlo.

Un canvas solo admite un tipo de contexto en toda su vida. Una vez que has pedido "2d", pedir "webgl" devuelve null para siempre, y al revés. No hay forma de liberar un contexto y pedir otro. Si necesitas ambos modelos, necesitas dos elementos.

Puede devolver null. Aparte del caso anterior, ocurre si el navegador no puede crear el búfer: canvas demasiado grande, memoria agotada, contexto perdido. No es frecuente pero tampoco es imposible, y un ctx nulo produce un error críptico varias líneas más abajo.

JS
const ctx = canvas.getContext('2d');if (!ctx) {  throw new Error('No se pudo obtener un contexto 2D en este canvas');}

El segundo parámetro de getContext es un objeto de atributos que condicionan cómo se crea el búfer. Merecen su propia lección; por ahora basta saber que se pasan solo en la primera llamada: en las siguientes se ignoran porque el contexto ya existe.

Los efectos secundarios de cambiar el tamaño

Esta es la parte que sorprende. Asignar canvas.width o canvas.height —incluso asignando el mismo valor que ya tenía— hace tres cosas a la vez:

  1. Redimensiona el búfer.
  2. Lo borra entero a negro transparente.
  3. Restablece todo el estado del contexto a sus valores por defecto: la matriz de transformación vuelve a la identidad, la región de recorte desaparece, fillStyle vuelve a negro, lineWidth a 1, se vacía la pila de save, y se descarta la ruta actual.
JS
ctx.fillStyle = '#f38ba8';ctx.scale(2, 2);canvas.width = canvas.width;   // no cambia el tamano, pero resetea todoconsole.log(ctx.fillStyle);    // '#000000'

Ese truco de reasignar el ancho se ha usado durante años como forma rápida de limpiar un canvas. Funciona, pero es una operación mucho más pesada que clearRect porque reasigna el búfer, y además destruye el estado, lo que hace que sea una fuente inagotable de bugs en código que usa transformaciones.

Desde hace varias versiones existe una forma explícita y limpia de hacer lo mismo sin tocar el tamaño:

JS
ctx.reset();   // borra el bitmap y restablece todo el estado

reset() está disponible en los tres motores desde 2023 y es lo que quieres cuando de verdad necesitas volver al punto de partida. Para limpiar solo los píxeles conservando el estado, clearRect sigue siendo la herramienta.

El arranque mínimo correcto

Juntando todo, este es el esqueleto que conviene tener memorizado. Funciona pegado tal cual.

HTML
<!doctype html><meta charset="utf-8"><style>  html, body { margin: 0; background: #1e1e2e; }  #lienzo { display: block; width: 600px; height: 400px; }</style><canvas id="lienzo" width="600" height="400">  Un cuadrado azul sobre fondo oscuro.</canvas><script>  const canvas = document.getElementById('lienzo');  const ctx = canvas.getContext('2d');  if (!ctx) throw new Error('Sin contexto 2D');  canvas.addEventListener('contextlost', e => e.preventDefault());  canvas.addEventListener('contextrestored', pintar);  function pintar() {    ctx.clearRect(0, 0, canvas.width, canvas.height);    ctx.fillStyle = '#89b4fa';    ctx.fillRect(100, 80, 400, 240);    ctx.strokeStyle = '#cba6f7';    ctx.lineWidth = 4;    ctx.strokeRect(100, 80, 400, 240);  }  pintar();</script>

Fíjate en el display: block de la regla CSS. Un <canvas> es un elemento en línea por defecto, lo que significa que se alinea sobre la línea base del texto y deja unos pocos píxeles de hueco debajo. Es un detalle cosmético que desconcierta a mucha gente y se arregla con display: block o con vertical-align: bottom.

Y fíjate en que el ancho y alto CSS coinciden exactamente con los atributos. Mientras coincidan y el dispositivo tenga densidad 1, todo se ve bien. En cuanto una de esas dos condiciones deja de cumplirse, el canvas se ve borroso, y esa es una historia que tiene su propio nivel.