wandres.dev
CANVAS: EL CONTEXTO 2D · Empezar a pintar

Un primer dibujo completo: rectángulos y limpieza

Escribir el primer canvas útil de principio a fin con los tres métodos de rectángulo, entender las diferencias entre ellos y ver el estado del contexto en acción.

⏱ 15 min

Los tres métodos de rectángulo del canvas —rellenar, trazar y limpiar— son los únicos que dibujan sin pasar por el modelo de rutas, y por eso son el mejor sitio para ver el estado del contexto funcionando sin más complicaciones. Con ellos y con lo que ya sabes se puede construir algo completo y honesto: un gráfico de barras que se dibuja desde datos.

🎯 Al terminar esta lección sabrás
  • Usar fillRect, strokeRect y clearRect sabiendo qué hace cada uno con el estado.
  • Explicar por qué estos tres métodos no necesitan beginPath.
  • Construir un gráfico de barras completo a partir de un array de datos.
  • Reconocer el patrón de separación entre datos, escala y dibujo.

Los tres métodos de rectángulo

ctx.fillRect(x, y, ancho, alto);    // rellena con fillStyle
ctx.strokeRect(x, y, ancho, alto);  // traza el contorno con strokeStyle y lineWidth
ctx.clearRect(x, y, ancho, alto);   // borra a negro transparente

Son los únicos métodos de dibujo que no tocan la ruta actual. Todos los demás —arc, lineTo, rect— construyen una ruta que hay que pintar después. Estos tres actúan de inmediato y no dejan rastro en el estado de rutas, lo que los hace seguros de usar en medio de la construcción de otra ruta sin efectos raros.

Sí respetan el resto del estado: la matriz de transformación, el recorte, globalAlpha, el modo de composición y las sombras. Un fillRect dentro de un contexto rotado sale rotado.

clearRect tiene una peculiaridad que hay que fijar: no pinta blanco, borra. Deja la zona en negro transparente, con alfa 0, de modo que se ve lo que haya detrás en la página. Si el contexto se creó con alpha: false, no hay transparencia posible y el resultado es negro opaco.

Y una segunda peculiaridad, mucho menos conocida: clearRect ignora el modo de composición, pero sí respeta el recorte. Es decir, si tienes una región de recorte activa, clearRect solo borra dentro de ella. Es un comportamiento útil que se usa poco.

// Limpiar todo el canvas, ignorando cualquier transformacion activa
function limpiarTodo(ctx) {
  ctx.save();
  ctx.setTransform(1, 0, 0, 1, 0, 0);   // matriz identidad
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  ctx.restore();
}

Esa función es de las que acaban en todos los proyectos. Sin el setTransform, un clearRect(0, 0, w, h) en un contexto escalado o trasladado deja zonas sin borrar, y el resultado son estelas fantasma que aparecen sin explicación aparente.

El estado se lee al dibujar, no al configurar

Un detalle conceptual que ordena mucho: las propiedades del contexto se consultan en el momento del dibujo, no se asocian a nada.

ctx.fillStyle = '#89b4fa';
ctx.fillRect(10, 10, 50, 50);
ctx.fillStyle = '#f38ba8';         // el rectangulo anterior NO cambia
ctx.fillRect(70, 10, 50, 50);

Esto parece obvio escrito así, y sin embargo es la fuente del error más frecuente en el aprendizaje: asignar un estilo esperando que se aplique a algo ya dibujado. En el canvas el estado es un conjunto de variables globales del contexto, y cada llamada de dibujo toma una foto de ellas.

De ahí sale también la primera regla de rendimiento del canvas: cambiar de estilo tiene coste, y agrupar los dibujos por estilo es más rápido que alternar.

// Peor: 200 cambios de estado
for (const b of barras) { ctx.fillStyle = b.color; ctx.fillRect(...); }

// Mejor cuando hay pocos colores: agrupar por color
for (const [color, grupo] of porColor) {
  ctx.fillStyle = color;
  for (const b of grupo) ctx.fillRect(b.x, b.y, b.w, b.h);
}

Un gráfico de barras completo

Con esto ya se puede construir algo real. Este ejemplo funciona pegado tal cual y contiene el patrón que se repite en toda visualización: datos, escala, dibujo, en tres bloques separados.

<!doctype html>
<meta charset="utf-8">
<style>
  body { background: #1e1e2e; color: #cdd6f4; font: 14px system-ui; margin: 2rem; }
  canvas { display: block; background: #181825; border-radius: 8px; }
</style>
<canvas id="g" width="640" height="360">
  <table>
    <caption>Ventas mensuales del primer semestre</caption>
    <tr><th>Mes</th><th>Ventas</th></tr>
    <tr><td>Ene</td><td>320</td></tr><tr><td>Feb</td><td>412</td></tr>
    <tr><td>Mar</td><td>288</td></tr><tr><td>Abr</td><td>501</td></tr>
    <tr><td>May</td><td>466</td></tr><tr><td>Jun</td><td>390</td></tr>
  </table>
</canvas>
<script>
  const ctx = document.getElementById('g').getContext('2d');

  // 1. DATOS
  const datos = [
    { mes: 'Ene', valor: 320 }, { mes: 'Feb', valor: 412 },
    { mes: 'Mar', valor: 288 }, { mes: 'Abr', valor: 501 },
    { mes: 'May', valor: 466 }, { mes: 'Jun', valor: 390 },
  ];

  // 2. ESCALA: del dominio de los datos al rango en pixeles
  const margen = { arriba: 24, derecha: 24, abajo: 40, izquierda: 52 };
  const W = ctx.canvas.width, H = ctx.canvas.height;
  const anchoTrama = W - margen.izquierda - margen.derecha;
  const altoTrama = H - margen.arriba - margen.abajo;
  const maximo = Math.max(...datos.map(d => d.valor));
  const techo = Math.ceil(maximo / 100) * 100;

  const escalaY = v => margen.arriba + altoTrama * (1 - v / techo);
  const paso = anchoTrama / datos.length;
  const anchoBarra = paso * 0.62;

  // 3. DIBUJO
  function pintar() {
    ctx.clearRect(0, 0, W, H);

    // Rejilla horizontal y etiquetas del eje
    ctx.strokeStyle = '#313244';
    ctx.fillStyle = '#7f849c';
    ctx.lineWidth = 1;
    ctx.textAlign = 'right';
    ctx.textBaseline = 'middle';
    ctx.font = '12px system-ui';
    for (let v = 0; v <= techo; v += techo / 5) {
      const y = Math.round(escalaY(v)) + 0.5;   // +0.5 para una linea nitida
      ctx.beginPath();
      ctx.moveTo(margen.izquierda, y);
      ctx.lineTo(W - margen.derecha, y);
      ctx.stroke();
      ctx.fillText(String(v), margen.izquierda - 10, y);
    }

    // Barras
    ctx.fillStyle = '#89b4fa';
    datos.forEach((d, i) => {
      const x = margen.izquierda + paso * i + (paso - anchoBarra) / 2;
      const y = escalaY(d.valor);
      ctx.fillRect(x, y, anchoBarra, margen.arriba + altoTrama - y);
    });

    // Etiquetas del eje X
    ctx.fillStyle = '#a6adc8';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'top';
    datos.forEach((d, i) => {
      ctx.fillText(d.mes, margen.izquierda + paso * (i + 0.5), H - margen.abajo + 10);
    });
  }

  pintar();
</script>

Fíjate en tres decisiones. La primera, que hay un objeto margen en lugar de números sueltos: el margen es lo que separa el área de trazado del borde del canvas, y necesita nombre porque aparece en seis sitios. La segunda, que existe una función escalaY que convierte un valor del dominio en una coordenada: es la abstracción central de cualquier visualización. Y la tercera, que la rejilla usa Math.round(...) + 0.5 para que las líneas de un píxel salgan nítidas en lugar de grises.

clearRect no siempre es lo más rápido, y a veces no es suficiente

Circulan dos afirmaciones contradictorias sobre cómo limpiar un canvas y las dos son parcialmente ciertas, lo cual explica la confusión. La primera dice que canvas.width = canvas.width es más rápido; la segunda, que clearRect lo es. En los navegadores actuales, clearRect sobre todo el lienzo es una operación que la GPU resuelve de forma trivial y es prácticamente siempre la mejor opción, mientras que reasignar el ancho destruye y recrea el búfer, lo cual además puede provocar una reasignación de memoria de vídeo y un tirón. Pero hay un caso concreto en el que clearRect no basta: cuando el contexto tiene una región de recorte activa, clearRect solo borra dentro de ella, así que un canvas que parecía limpio conserva contenido fuera del recorte. Y hay un segundo caso, mucho más sutil: si el canvas se creó con alpha: false, clearRect deja negro opaco, no transparente, y si tu diseño esperaba ver el fondo de la página a través, no lo verá. La rutina segura es la que restaura la matriz y quita el recorte antes de limpiar, y eso solo se consigue envolviendo en save/restore o llamando a ctx.reset(), que es la única operación que garantiza volver al punto de partida. Lo caro de reset() es que también tira los estilos, así que hay que reconfigurar. Escribe esas tres variantes de limpieza una vez, ponlas en un módulo, y no vuelvas a pensarlo.

Qué falta

Ese gráfico funciona, pero tiene tres carencias que el resto de la guía va a ir cubriendo, y conviene nombrarlas ahora para que sepas que no se te han pasado por alto.

Se ve borroso en pantallas de alta densidad, porque el búfer tiene 640 por 360 píxeles y en un portátil moderno ese elemento ocupa el doble de píxeles físicos. Es el problema del que se ocupa el nivel siguiente.

No se adapta al tamaño del contenedor: los números están fijos. Adaptarlo bien exige observar el tamaño real del elemento y reconstruir el búfer.

No tiene interacción: no hay tooltip al pasar el ratón, porque para eso hace falta hit testing, y el hit testing en canvas es un tema con entidad propia.

Ninguna de las tres es un fallo del código: son exactamente las tres cosas que el modo inmediato deja en tus manos.