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étodo | Descripció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 imagen | Descripción |
---|---|
HTMLImageElement | Una elemento <img> de HTML con una imagen en formatos como jpg , png , webp , avif , gif ... |
SVGImageElement | Una elemento <svg> de HTML con una imagen vectorial. |
HTMLVideoElement | Un elemento <video> de HTML con un video en formatos como mp4 , webm ... |
HTMLCanvasElement | Otro elemento <canvas> de HTML utilizado como "fuente". |
ImageBitmap | Una imagen de mapa de bits, normalmente creada para trabajar sin latencia. |
OffscreenCanvas | Un elemento <canvas> que no está atado al DOM y puede estar fuera de pantalla. |
VideoFrame | Un 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
:
- 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:
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.