Saltar al contenido

Los operadores de Porter-Duff

Recorrer los doce operadores de composición clásicos, entender la lógica que los organiza y saber cuál elegir para cada efecto de máscara o recorte.

La lógica de los nombres

Los operadores se organizan en un esquema que hace innecesario memorizarlos. La primera palabra dice qué capa sobrevive; el sufijo dice dónde.

  • source-*: sobrevive el origen, lo que estás dibujando.
  • destination-*: sobrevive el destino, lo que ya había.
  • -over: en toda su extensión, encima de la otra.
  • -in: solo donde la otra capa también está.
  • -out: solo donde la otra capa no está.
  • -atop: en la extensión de la otra capa.

Con esas seis piezas se generan ocho operadores, y a ellos se suman cuatro casos especiales.

Operador Qué queda
source-over Origen encima del destino. Por defecto.
source-in Origen solo donde hay destino. El destino desaparece.
source-out Origen solo donde no hay destino.
source-atop Origen recortado a la forma del destino, sobre el destino.
destination-over Destino encima del origen: el origen queda debajo.
destination-in Destino solo donde hay origen.
destination-out Destino solo donde no hay origen: el origen borra.
destination-atop Destino recortado a la forma del origen, sobre el origen.
lighter Suma de ambos: los colores se acumulan.
copy Solo el origen; el destino se descarta entero.
xor Ambos, salvo donde se solapan, que queda transparente.
source-over con alfa El caso general de la mezcla.

Los tres que se usan de verdad

De los doce, tres resuelven la práctica totalidad de los casos reales.

destination-out: el borrador. Lo que dibujes borra lo que había, con la forma que dibujes y respetando su antialiasing. Es la herramienta de goma de borrar de cualquier aplicación de dibujo, y también la forma de hacer agujeros de cualquier forma.

JS
// Goma de borrar con borde suavectx.globalCompositeOperation = 'destination-out';ctx.beginPath();ctx.arc(x, y, 24, 0, Math.PI * 2);ctx.fill();ctx.globalCompositeOperation = 'source-over';

destination-in: la máscara. Conserva del destino solo lo que coincide con el origen. Es la forma de aplicar una máscara arbitraria a una imagen ya dibujada.

JS
// Recortar una foto con una forma cualquieractx.drawImage(foto, 0, 0);ctx.globalCompositeOperation = 'destination-in';ctx.beginPath();ctx.roundRect(20, 20, 200, 200, 40);ctx.fill();ctx.globalCompositeOperation = 'source-over';

Frente a clip, esta técnica tiene dos ventajas: la máscara puede tener transparencia parcial —un degradado como máscara produce un desvanecido— y se puede construir de forma acumulativa dibujando varias formas antes de aplicarla. Y una desventaja: afecta a todo el canvas, así que hay que hacerlo en una capa aparte.

source-atop: pintar dentro. Lo que dibujes solo aparece donde ya había algo. Es la forma de colorear, texturizar o iluminar una silueta existente sin salirse.

JS
// Tenir una silueta ya dibujadactx.drawImage(sprite, 0, 0);ctx.globalCompositeOperation = 'source-atop';ctx.fillStyle = 'rgba(243,139,168,0.6)';ctx.fillRect(0, 0, W, H);              // el rectangulo solo tine el spritectx.globalCompositeOperation = 'source-over';

lighter: la luz que se suma

lighter no es de la familia de Porter-Duff estricta pero se agrupa con ellos, y su comportamiento es la suma de los dos colores, con saturación en 255.

Eso lo convierte en el operador de la luz: dos luces superpuestas dan más luz, nunca menos. Es exactamente cómo se comporta la luz física y es la razón de que sea el operador de partículas, destellos, estelas y efectos de brillo.

JS
// Nube de particulas luminosasctx.globalCompositeOperation = 'lighter';for (const p of particulas) {  const g = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.r);  g.addColorStop(0, `rgba(137,180,250,${p.vida})`);  g.addColorStop(1, 'rgba(137,180,250,0)');  ctx.fillStyle = g;  ctx.fillRect(p.x - p.r, p.y - p.r, p.r * 2, p.r * 2);}ctx.globalCompositeOperation = 'source-over';

Ese ejemplo tiene un problema de rendimiento que ya conoces —crea un degradado por partícula y por fotograma— y la solución también: precalcular un degradado radial en un canvas pequeño y dibujarlo escalado con drawImage, que además es mucho más rápido.

JS
// Textura de particula, creada UNA vezconst textura = document.createElement('canvas');textura.width = textura.height = 64;(() => {  const c = textura.getContext('2d');  const g = c.createRadialGradient(32, 32, 0, 32, 32, 32);  g.addColorStop(0, 'rgba(255,255,255,1)');  g.addColorStop(1, 'rgba(255,255,255,0)');  c.fillStyle = g; c.fillRect(0, 0, 64, 64);})();// En el bucle: solo drawImagectx.globalCompositeOperation = 'lighter';for (const p of particulas) {  ctx.globalAlpha = p.vida;  ctx.drawImage(textura, p.x - p.r, p.y - p.r, p.r * 2, p.r * 2);}ctx.globalAlpha = 1;ctx.globalCompositeOperation = 'source-over';

Una utilidad de capa

JS
/** * Compone un efecto en una capa aparte y lo trae al canvas principal. * dibujarCapa recibe un contexto limpio del tamano indicado. */function enCapa(ctx, x, y, ancho, alto, dibujarCapa, dpr = 1) {  const capa = document.createElement('canvas');  capa.width = Math.ceil(ancho * dpr);  capa.height = Math.ceil(alto * dpr);  const c = capa.getContext('2d');  c.setTransform(dpr, 0, 0, dpr, 0, 0);  dibujarCapa(c, ancho, alto);  ctx.drawImage(capa, x, y, ancho, alto);  capa.width = capa.height = 0;        // liberar el buffer}// Uso: una foto con mascara de degradado, sin tocar el resto del canvasenCapa(ctx, 40, 40, 240, 180, (c, w, h) => {  c.drawImage(foto, 0, 0, w, h);  c.globalCompositeOperation = 'destination-in';  const g = c.createLinearGradient(0, 0, 0, h);  g.addColorStop(0, 'rgba(0,0,0,1)');  g.addColorStop(1, 'rgba(0,0,0,0)');  c.fillStyle = g;  c.fillRect(0, 0, w, h);}, window.devicePixelRatio || 1);

Crear un canvas por efecto y por fotograma es caro; en un bucle de animación hay que reutilizar la misma capa, limpiándola con clearRect en lugar de crearla de nuevo. La versión de arriba es correcta para efectos puntuales y hay que adaptarla para el bucle.