Saltar al contenido

imageSmoothing: cuándo suavizar y cuándo no

Controlar el filtrado del canvas al escalar imágenes, conseguir pixel art nítido de verdad, y aplicar la reducción por pasos para miniaturas de calidad.

Las dos propiedades

JS
ctx.imageSmoothingEnabled = false;         // vecino mas proximoctx.imageSmoothingQuality = 'high';        // 'low' | 'medium' | 'high'

imageSmoothingEnabled es una decisión binaria: con false, cualquier escalado usa el vecino más próximo y produce bloques duros; con true, se interpola.

imageSmoothingQuality solo tiene efecto cuando el suavizado está activo, y es una pista: el navegador puede ignorarla y los tres valores no siempre se distinguen. Pedir 'high' en una reducción grande sí produce diferencias visibles en los navegadores actuales.

Ambas forman parte del estado del contexto y las guarda save.

Lo que afectan y lo que no importa mucho:

Operación ¿Le afecta el suavizado?
drawImage con escalado Sí
drawImage sin escalado y en posición entera No: es copia directa
Patrones escalados con createPattern Sí
putImageData No, nunca: escribe píxel a píxel
Rutas y texto No: tienen su propio antialiasing, que no se desactiva

Esa última fila merece énfasis porque es un malentendido frecuente: imageSmoothingEnabled = false no desactiva el antialiasing de las formas. Un círculo trazado sigue teniendo bordes suaves. La propiedad solo gobierna el remuestreo de imágenes. No hay ninguna forma estándar de desactivar el antialiasing de rutas en el canvas 2D.

Pixel art nítido, de verdad

El caso obvio:

JS
ctx.imageSmoothingEnabled = false;ctx.drawImage(sprite, 0, 0, sprite.width * 4, sprite.height * 4);

Y el caso que falla en la mitad de los dispositivos: si el contexto está escalado por la densidad de pantalla, la escala total hasta el píxel físico es el producto de tu factor por la densidad. Con un factor de 4 y densidad 1,5, la escala real es 6, que es entera y va bien. Con densidad 2,625 —muchos Android— la escala real es 10,5, y aunque el suavizado esté desactivado, el compositor del navegador remuestrea al componer y aparecen filas de píxeles de distinto grosor.

La defensa es calcular la escala en píxeles físicos y redondearla a entero:

JS
function dibujarPixelArt(ctx, sprite, x, y, escalaDeseada, dpr) {  const escalaReal = Math.max(1, Math.round(escalaDeseada * dpr));  const escalaLogica = escalaReal / dpr;  ctx.imageSmoothingEnabled = false;  // Posicion redondeada al pixel FISICO  const px = Math.round(x * dpr) / dpr;  const py = Math.round(y * dpr) / dpr;  ctx.drawImage(sprite, px, py,                sprite.width * escalaLogica, sprite.height * escalaLogica);}

Con eso, el sprite ocupa un número entero de píxeles físicos por píxel de arte y cada bloque sale idéntico. El precio es que el tamaño en pantalla no es exactamente el que pediste, y hay que diseñar el layout para tolerar ese ajuste. Es exactamente lo que hacen los emuladores y los juegos de píxeles bien hechos: en lugar de rellenar la ventana, eligen el múltiplo entero más grande que cabe y dejan bandas.

JS
// Escalado entero que llena lo maximo posible sin distorsionfunction escalaEntera(anchoDisponible, altoDisponible, anchoArte, altoArte) {  return Math.max(1, Math.floor(Math.min(anchoDisponible / anchoArte,                                         altoDisponible / altoArte)));}

Reducir bien

En el extremo contrario está la reducción de fotografías, donde el problema no es el filtrado sino el aliasing. Reducir de golpe un factor grande produce moirés y detalle sucio, porque el filtro del navegador muestrea muy pocos píxeles de origen por cada píxel de destino.

La solución es reducir en etapas de un factor dos, cada una de las cuales sí está bien filtrada por la interpolación bilineal:

JS
function reducirPorPasos(origen, anchoFinal, altoFinal) {  let actual = origen;  let w = origen.width ?? origen.naturalWidth;  let h = origen.height ?? origen.naturalHeight;  while (w * 0.5 > anchoFinal && h * 0.5 > altoFinal) {    w = Math.max(anchoFinal, Math.round(w * 0.5));    h = Math.max(altoFinal, Math.round(h * 0.5));    const paso = document.createElement('canvas');    paso.width = w; paso.height = h;    const c = paso.getContext('2d');    c.imageSmoothingEnabled = true;    c.imageSmoothingQuality = 'high';    c.drawImage(actual, 0, 0, w, h);    if (actual !== origen) { actual.width = 0; actual.height = 0; }  // liberar    actual = paso;  }  const fin = document.createElement('canvas');  fin.width = anchoFinal; fin.height = altoFinal;  const c = fin.getContext('2d');  c.imageSmoothingEnabled = true;  c.imageSmoothingQuality = 'high';  c.drawImage(actual, 0, 0, anchoFinal, altoFinal);  if (actual !== origen) { actual.width = 0; actual.height = 0; }  return fin;}

La diferencia frente a un drawImage directo es visible a simple vista en cualquier fotografía con detalle fino: tejidos, follaje, texto pequeño. Es la técnica de las bibliotecas de miniaturas serias.

Existe una alternativa que a menudo es mejor y más barata: createImageBitmap con resizeQuality: 'high', que delega el reescalado al navegador y en varios motores usa un filtro de mejor calidad que el de drawImage. Prueba las dos con tus imágenes antes de escribir la reducción por pasos.

El CSS que acompaña

Para cerrar el círculo, el elemento canvas también admite la propiedad de renderizado de imagen, que gobierna el escalado del búfer a la caja:

CSS
canvas.pixel {  image-rendering: pixelated;   /* vecino mas proximo en el compositor */}

Sin esa regla, un canvas con el búfer más pequeño que su caja se suaviza al componerse aunque hayas desactivado el suavizado dentro. Con ella, el escalado final también es de bloques duros.

Esa combinación —búfer pequeño, image-rendering: pixelated, y CSS que agranda el elemento— es en realidad una técnica muy eficiente para pixel art: el búfer tiene el tamaño de la resolución nativa del juego, dibujas con coordenadas de esa resolución sin ninguna escala, y el navegador se encarga de ampliar. Es menos memoria, menos píxeles que dibujar y menos aritmética.

HTML
<style>  #pantalla { width: 100%; image-rendering: pixelated; }</style><canvas id="pantalla" width="320" height="180"></canvas>

Trescientos veinte por ciento ochenta píxeles de búfer, mostrados a pantalla completa, con bloques perfectamente nítidos y un coste de dibujo ridículo. Es el reverso exacto del consejo habitual de dimensionar el búfer a los píxeles físicos, y es correcto precisamente porque el arte no tiene más resolución que esa.