Canvas 2D en JavaScript: dibujar, transformar, animaciones con requestAnimationFrame

La API Canvas 2D de JavaScript permite dibujar gráficos, imágenes, texto y formas en el navegador de forma inmediata (immediate mode): en lugar de mantener una lista de objetos como SVG, dibujas directamente sobre un bitmap pixel a pixel. Esto la hace idónea para animaciones complejas, juegos 2D, procesamiento de imágenes y cualquier cosa donde necesites control total sobre cada pixel renderizado.

Configurar el canvas y el contexto

El punto de entrada es el elemento <canvas> y su método getContext('2d'). Los tamaños del canvas en CSS y en atributos son independientes: los atributos definen la resolución real del bitmap, el CSS define el tamaño visual.

const canvas = document.getElementById('mi-canvas');
const ctx = canvas.getContext('2d');

// Para pantallas de alta densidad (Retina)
const dpr = window.devicePixelRatio || 1;
canvas.width = canvas.clientWidth * dpr;
canvas.height = canvas.clientHeight * dpr;
ctx.scale(dpr, dpr); // escalar el contexto para que el código use píxeles CSS

Formas básicas: rect, arc y paths

// Rectángulo relleno
ctx.fillStyle = '#3498db';
ctx.fillRect(10, 10, 200, 100);

// Rectángulo con borde
ctx.strokeStyle = '#e74c3c';
ctx.lineWidth = 3;
ctx.strokeRect(220, 10, 200, 100);

// Círculo
ctx.beginPath();
ctx.arc(150, 200, 50, 0, Math.PI * 2);
ctx.fillStyle = '#2ecc71';
ctx.fill();

// Path personalizado (triángulo)
ctx.beginPath();
ctx.moveTo(300, 250);
ctx.lineTo(250, 150);
ctx.lineTo(350, 150);
ctx.closePath();
ctx.fillStyle = '#9b59b6';
ctx.fill();

save/restore: aislar transformaciones y estilos

ctx.save() y ctx.restore() guardan y restauran el estado completo del contexto (transformaciones, estilos, clip). Úsalos siempre que apliques transformaciones temporales para no contaminar el estado global:

function dibujarRotado(ctx, x, y, ancho, alto, angulo) {
  ctx.save();
  ctx.translate(x + ancho / 2, y + alto / 2); // mover al centro del rectángulo
  ctx.rotate(angulo);
  ctx.fillStyle = '#e67e22';
  ctx.fillRect(-ancho / 2, -alto / 2, ancho, alto);
  ctx.restore(); // volver al estado anterior
}

dibujarRotado(ctx, 100, 100, 120, 60, Math.PI / 6); // 30 grados
dibujarRotado(ctx, 300, 100, 80, 80, Math.PI / 4);  // 45 grados

Game loop con requestAnimationFrame

requestAnimationFrame ejecuta la función de animación justo antes del siguiente repintado del navegador, sincronizando el juego o animación con la frecuencia de refresco de la pantalla (60Hz, 120Hz, etc.):

class Pelota {
  constructor(x, y, radio, vx, vy) {
    this.x = x; this.y = y;
    this.radio = radio;
    this.vx = vx; this.vy = vy;
  }

  actualizar(ancho, alto) {
    this.x += this.vx;
    this.y += this.vy;
    if (this.x - this.radio < 0 || this.x + this.radio > ancho) this.vx *= -1;
    if (this.y - this.radio < 0 || this.y + this.radio > alto) this.vy *= -1;
  }

  dibujar(ctx) {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radio, 0, Math.PI * 2);
    ctx.fillStyle = '#e74c3c';
    ctx.fill();
  }
}

const pelota = new Pelota(200, 200, 20, 3, 2);

function loop() {
  ctx.clearRect(0, 0, canvas.width, canvas.height); // limpiar frame anterior
  pelota.actualizar(canvas.clientWidth, canvas.clientHeight);
  pelota.dibujar(ctx);
  requestAnimationFrame(loop); // solicitar el siguiente frame
}

requestAnimationFrame(loop);

Sprite animation con drawImage

ctx.drawImage permite dibujar una imagen o un recorte de ella (sprite sheet), fundamental para animaciones 2D con fotogramas:

const spriteSheet = new Image();
spriteSheet.src = '/assets/personaje.png';

const FRAME_WIDTH = 64;
const FRAME_HEIGHT = 64;
const TOTAL_FRAMES = 8;
let frameActual = 0;
let lastTime = 0;
const FPS_ANIMACION = 12;

spriteSheet.onload = () => requestAnimationFrame(animarSprite);

function animarSprite(tiempo) {
  if (tiempo - lastTime > 1000 / FPS_ANIMACION) {
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh)
    ctx.drawImage(
      spriteSheet,
      frameActual * FRAME_WIDTH, 0,  // recorte en el sprite sheet
      FRAME_WIDTH, FRAME_HEIGHT,
      100, 100,                       // posición en el canvas
      FRAME_WIDTH * 2, FRAME_HEIGHT * 2 // tamaño escalado x2
    );

    frameActual = (frameActual + 1) % TOTAL_FRAMES;
    lastTime = tiempo;
  }
  requestAnimationFrame(animarSprite);
}

OffscreenCanvas con Web Workers

OffscreenCanvas permite renderizar en un Worker, liberando el hilo principal de trabajo de pintura costoso:

// main.js
const canvas = document.getElementById('canvas');
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker('./render-worker.js');
worker.postMessage({ canvas: offscreen }, [offscreen]);

// render-worker.js
self.onmessage = ({ data: { canvas } }) => {
  const ctx = canvas.getContext('2d');
  let frame = 0;

  function loop() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = `hsl(${frame % 360}, 70%, 50%)`;
    ctx.fillRect(50, 50, 200, 200);
    frame++;
    requestAnimationFrame(loop);
  }

  requestAnimationFrame(loop);
};

La API Canvas 2D tiene más de 30 métodos y propiedades de estado. Los que más se usan son los que cubren este artículo: las formas básicas, las transformaciones con save/restore, el game loop con requestAnimationFrame y la manipulación de imágenes con drawImage. Para escenas 3D, la API WebGL (y WebGPU en navegadores modernos) ofrece acceso a la GPU directamente desde JavaScript.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP