Las tres firmas de drawImage
Dominar el método más versátil del canvas: sus tres formas de llamada, las fuentes que acepta, y el comportamiento con imágenes que aún no han cargado.
Las tres firmas
ctx.drawImage(fuente, dx, dy);ctx.drawImage(fuente, dx, dy, dAncho, dAlto);ctx.drawImage(fuente, sx, sy, sAncho, sAlto, dx, dy, dAncho, dAlto);La primera dibuja la imagen a su tamaño natural con la esquina superior izquierda en (dx, dy).
La segunda la escala para que ocupe el rectángulo de destino. No conserva la proporción: si la das deformada, sale deformada.
La tercera recorta una región del origen y la dibuja en un rectángulo de destino. Los cuatro primeros parámetros describen el recorte en coordenadas de la fuente; los cuatro últimos, el destino en coordenadas del espacio de usuario actual.
Esa asimetría es importante y es fuente de confusión: el rectángulo de origen se mide en píxeles de la imagen original y no se ve afectado por la matriz del contexto; el de destino sí. Si tienes el contexto escalado por la densidad de pantalla, las coordenadas de origen siguen siendo píxeles de la imagen y las de destino son unidades lógicas.
// Recortar la mitad derecha de una imagen de 800x600 y dibujarla a 200x150ctx.drawImage(img, 400, 0, 400, 600, // origen: mitad derecha, en pixeles de la imagen 20, 20, 200, 150); // destino: en unidades del contextoLas fuentes aceptadas
drawImage acepta cualquier CanvasImageSource, que engloba:
HTMLImageElement, es decir, una<img>o unnew Image().SVGImageElement.HTMLVideoElement: dibuja el fotograma actual del vídeo.HTMLCanvasElement: otro canvas.ImageBitmap.OffscreenCanvas.VideoFrame, de la API de códecs web.
De esa lista, tres merecen comentario.
Un vídeo dibuja el fotograma que se esté mostrando en ese instante. Es la puerta de entrada al procesamiento de vídeo y de cámara en el navegador: cada fotograma que dibujas en el canvas puedes analizarlo o modificarlo.
Otro canvas es la base de la técnica de caché: dibujas una vez algo costoso en un canvas auxiliar y después lo copias tantas veces como quieras con una operación barata.
Un ImageBitmap es la fuente más eficiente de todas, porque ya está decodificada y en un formato listo para la GPU. Merece su propia lección.
Cuando no dibuja nada
drawImage es de los pocos métodos del canvas que puede lanzar excepciones, y también de los que fallan en silencio. Conviene tener claros los cuatro casos.
La imagen no ha cargado. Si el estado de la imagen es “no disponible” —img.complete es falso o naturalWidth es cero— la llamada no hace nada y no lanza. Es el caso más frecuente y el más desconcertante, porque el código es correcto y no pasa nada.
Un rectángulo con ancho o alto cero. No dibuja y no lanza.
Un valor no finito en cualquier parámetro. La llamada se ignora silenciosamente, igual que en el resto de la API.
El rectángulo de origen se sale de la imagen. Aquí sí hay dos comportamientos: si el recorte queda completamente fuera, se lanza un IndexSizeError; si queda parcialmente fuera, se recorta a la intersección y se dibuja lo que hay, escalando en consecuencia.
La defensa completa cabe en una función:
function dibujarSeguro(ctx, img, ...args) { if (!img) return false; const listo = img instanceof HTMLImageElement ? img.complete && img.naturalWidth > 0 : true; if (!listo) return false; try { ctx.drawImage(img, ...args); return true; } catch { return false; }}Cargar una imagen bien
El patrón correcto usa promesas y contempla el error:
function cargarImagen(url) { return new Promise((resolver, rechazar) => { const img = new Image(); img.onload = () => resolver(img); img.onerror = () => rechazar(new Error('No se pudo cargar ' + url)); img.src = url; });}const logo = await cargarImagen('/logo.png');ctx.drawImage(logo, 20, 20);Dos detalles que hay que fijar. El primero: asigna src después de los manejadores. Con una imagen ya en caché, el navegador puede resolver la carga de forma síncrona en algunos motores, y un manejador asignado después no se dispararía.
El segundo: para imágenes de otro origen que vayas a leer píxeles después, hay que pedir CORS antes de asignar el src:
const img = new Image();img.crossOrigin = 'anonymous'; // ANTES de srcimg.src = 'https://otro-dominio.example/foto.jpg';Sin eso, dibujar la imagen funciona, pero el canvas queda contaminado y cualquier lectura posterior —getImageData, toDataURL, toBlob— lanza un SecurityError. Es una protección contra la exfiltración de contenido de otro origen y no tiene ninguna forma de rodearse desde el cliente: el servidor tiene que enviar la cabecera adecuada.
Un ejemplo con ajuste de proporción
Un caso constante en la práctica es dibujar una imagen dentro de una caja conservando su relación de aspecto, con los dos comportamientos de object-fit:
/** modo: 'contain' cabe entera con bandas; 'cover' llena y recorta. */function dibujarAjustado(ctx, img, x, y, w, h, modo = 'cover') { const iw = img.naturalWidth ?? img.width; const ih = img.naturalHeight ?? img.height; const escala = modo === 'cover' ? Math.max(w / iw, h / ih) : Math.min(w / iw, h / ih); const dw = iw * escala, dh = ih * escala; const dx = x + (w - dw) / 2, dy = y + (h - dh) / 2; if (modo === 'cover') { // Recortar en origen es mejor que confiar en el clip: menos pixeles movidos const sw = w / escala, sh = h / escala; const sx = (iw - sw) / 2, sy = (ih - sh) / 2; ctx.drawImage(img, sx, sy, sw, sh, x, y, w, h); } else { ctx.drawImage(img, dx, dy, dw, dh); }}Fíjate en que el modo cover se resuelve recortando en el origen en lugar de dibujar de más y recortar con clip. Es más rápido porque el navegador solo remuestrea los píxeles que van a verse, y evita tener que gestionar una región de recorte.