Imágenes con <canvas>

Trabajando con imágenes en elementos canvas


Uno de los detalles más interesantes de trabajar con <canvas> es que puedes dibujar imágenes en el lienzo, utilizando la potencia de Javascript para realizar cualquier tarea adicional.

Para ello, utilizaremos el método .drawImage() que podemos utilizarlo de varias formas diferentes:

MétodoDescripción
ctx.drawImage(image, x, y)Dibuja la imagen en las coordenadas x,y.
ctx.drawImage(image, x, y, w, h)Dibuja la imagen en x,y con tamaño WxH.
ctx.drawImage(image, frame_x, frame_y, frame_w, frame_h, x, y, w, h)Dibuja un frame concreto en el lienzo.

Además, ten en cuenta que en image podemos indicar varios tipos de elementos, no solo imágenes:

Tipo de imagenDescripción
HTMLImageElementUna elemento <img> de HTML con una imagen en formatos como jpg, png, webp, avif, gif...
SVGImageElementUna elemento <svg> de HTML con una imagen vectorial.
HTMLVideoElementUn elemento <video> de HTML con un video en formatos como mp4, webm...
HTMLCanvasElementOtro elemento <canvas> de HTML utilizado como "fuente".
ImageBitmapUna imagen de mapa de bits, normalmente creada para trabajar sin latencia.
OffscreenCanvasUn elemento <canvas> que no está atado al DOM y puede estar fuera de pantalla.
VideoFrameUn fotograma de un video específico.

Primero, vamos a centrarnos en las dos primeras trabajando con una imagen simple, ya que son las más sencillas. Luego, trabajaremos con un spritesheet (imagen con varios frames) y utilizaremos la última.

Dibujo de imágenes

Vamos a utilizar la siguiente imagen de una gallina para dibujarla en el lienzo. Esta imagen tiene un tamaño de 32x32 píxels, mientras que nuestro canvas tiene un tamaño de 200x200:

Imagen de una gallina

  • 1️⃣ El primer drawImage() dibuja la imagen con el mismo tamaño.
  • 2️⃣ El segundo drawImage() dibuja la imagen con el tamaño indicado al final.
<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";

  const chicken = document.createElement("img");
  chicken.src = "chicken.png";

  // Cuando la imagen haya cargado...
  chicken.addEventListener("load", () => {
    ctx.drawImage(chicken, 80, 45);               // Mismas dimensiones (32x32)

    ctx.imageSmoothingEnabled = false;            // Respeta pixel art
    ctx.drawImage(chicken, 65, 110, 64, 64);      // Ampliación x2      (64x64)
  });
</script>

Un fallo muy común al dibujar imágenes en canvas es pensar que la imagen está cargada inmediatamente. Esa tarea depende de la conexión, por lo que hay que esperar a que esté cargada. Para evitarlo, escucharemos un evento load para asegurarnos.

Observa que en nuestro caso concreto estamos usando imágenes pixel art. Hemos desactivado imageSmoothingEnabled para que no suavice la imagen al ampliarla, ya que en las imágenes pixel art no se usa ese suavizado.

Animaciones con Sprites

Observa el siguiente spritesheet (imagen con varios fotogramas). En él se muestran 8 fotogramas, divididos en dos filas de 4 fotogramas. La primera fila es la animación del pollo en idle, es decir cuando está quieto y la segunda fila es la animación del pollo caminando:

Spritesheet de una gallina

Dos detalles importantes sobre la animación de este spritesheet es como saltar de un frame a otro, y como acelerar o retrasar la velocidad de la animación. Vamos a explicarlo paso a paso:

Para que se produzca la animación, debemos movernos de uno a otro en esos cuatro frames, de modo que cuando termine vuelva a empezar. Una forma sencilla de hacer esto, puede hacerse contabilizando sus frames:

let frame = 0;
const TOTAL_FRAMES = 4;

// update
let frame = (frame + 1) % TOTAL_FRAMES;

// draw
const frame_x = frame * frameSize

Observa que en frame indicamos el frame donde nos encontramos (el primero). En la parte de actualización, vamos rotando usando el módulo. Eso significa que va a seguir el orden 0, 1, 2, 3, 0, 1, 2, 3.... Finalmente, en la parte de renderizado, simplemente multiplicamos el frame por el tamaño del frame.

Por otro lado, lo habitual es que la velocidad de la animación sea muy rápida. Podemos crear un pequeño fragmento de código que actualice sólo cuando pasen varios frames, así ralentizaríamos la animación:

const FRAME_MAX_DELAY = 10;
let delay = 0;

// update
delay++;

if (delay >= FRAME_MAX_DELAY) {
  frame = (frame + 1) % TOTAL_FRAMES;
  delay = 0;
}

Observa que con FRAME_MAX_DELAY establecemos el número al que debe llegar el contador para avanzar un frame. Luego, en la parte de actualización, simplemente sólo avanzamos frame si el contador ha superado el valor máximo.

Una vez mencionados estos detalles, es hora de implementarlo todo junto. Veamos el ejemplo completo:

<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";
  ctx.imageSmoothingEnabled = false;

  const chicken = document.createElement("img");
  chicken.src = "chicken-sprite.png";

  // Inicialización
  const frameSize = 32;
  const x = 65, y = 75;
  let frame = 0;
  const TOTAL_FRAMES = 4;
  const FRAME_MAX_DELAY = 10;
  let delay = 0;

  const update = () => {
    delay++;

    if (delay >= FRAME_MAX_DELAY) {
      frame = (frame + 1) % TOTAL_FRAMES;
      delay = 0;
    }
  }

  const draw = () => {
    const frame_x = frame * frameSize;
    const finalSize = frameSize * 2;
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.drawImage(
      chicken,
      frame_x, 0, // Coordenadas de recorte (source x,y)
      frameSize, frameSize, // Tamaño del recorte (WxH)
      x, y,                 // Posición en el canvas (destination x,y)
      finalSize, finalSize  // Tamaño en el canvas (WxH)
    );
  }

  function gameLoop() {
    update();
    draw();
    requestAnimationFrame(gameLoop);
  }

  // Cuando la imagen haya cargado...
  chicken.addEventListener("load", () => gameLoop());
</script>

Recuerda que si estás buscando hacer algo más grande de forma más rápida y simple, una buena recomendación es utilizar Phaser, que te permite hacer muchas cosas con menos código.

¿Quién soy yo?

Soy Manz, vivo en Tenerife (España) y soy streamer partner en Twitch y profesor. Me apasiona el universo de la programación web, el diseño y desarrollo web y la tecnología en general. Aunque soy full-stack, mi pasión es el front-end, la terminal y crear cosas divertidas y locas.

Puedes encontrar más sobre mi en Manz.dev