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.
// 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.
// 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.
// 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.
// 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.
// 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
/** * 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.