El modelo de composición: origen, destino y globalAlpha
Entender qué son el origen y el destino en cada operación de dibujo, cómo se combinan por defecto, y qué papel juega globalAlpha en la ecuación.
Origen y destino
En toda operación de composición hay dos imágenes.
El destino es lo que ya hay en el canvas antes de la operación.
El origen es lo que estás dibujando ahora: la forma rellena, el texto, la imagen.
El operador decide cómo se combinan. La propiedad que lo controla es:
ctx.globalCompositeOperation = 'source-over'; // valor por defectoCon source-over, el origen se pinta encima del destino respetando su transparencia. Es lo que espera todo el mundo y por eso pasa desapercibido.
La ecuación, con los colores en alfa premultiplicado, es una interpolación:
resultado = origen + destino × (1 − alfaOrigen)Es decir: el color del origen se suma tal cual, y el del destino se atenúa en proporción a lo que el origen tape. Con alfa de origen igual a 1 el destino desaparece por completo; con alfa 0 el destino queda intacto.
Esa fórmula es la que hace que el alfa premultiplicado sea tan cómodo: no hay ninguna multiplicación repetida, solo una suma y un producto. Es la razón de que prácticamente todos los motores de composición trabajen en ese espacio.
globalAlpha
ctx.globalAlpha = 0.5; // de 0 a 1globalAlpha multiplica el alfa de todo lo que dibujes después de asignarlo. Se aplica antes de la composición, así que afecta al origen y no al destino.
Hay una diferencia importante entre globalAlpha y usar un color semitransparente, y es sutil pero real:
// A: color con alfactx.fillStyle = 'rgba(137,180,250,0.5)';ctx.fillRect(20, 20, 100, 100);ctx.fillRect(70, 70, 100, 100); // la zona solapada se ve mas opaca// B: globalAlphactx.globalAlpha = 0.5;ctx.fillStyle = '#89b4fa';ctx.fillRect(220, 20, 100, 100);ctx.fillRect(270, 70, 100, 100); // igual: la zona solapada tambienEn este caso concreto son equivalentes, porque cada fillRect es una operación de composición independiente. Pero cuando el origen es una forma compleja con partes solapadas —una ruta con varias subrutas que se cruzan, o un drawImage de algo con transparencia interna— la diferencia aparece: globalAlpha se aplica al resultado del origen ya compuesto internamente, mientras que un color con alfa se aplica a cada primitiva.
La regla que se deriva: para atenuar un grupo entero como una unidad, globalAlpha no basta. Hay que componer el grupo en un canvas auxiliar y dibujarlo después con globalAlpha, que es exactamente lo que hace la propiedad opacity de CSS con un grupo de elementos.
// Atenuar un grupo como una unidadconst capa = document.createElement('canvas');capa.width = W; capa.height = H;const cctx = capa.getContext('2d');dibujarGrupo(cctx); // las partes se solapan a plena opacidadctx.globalAlpha = 0.5;ctx.drawImage(capa, 0, 0); // el grupo entero al 50 %ctx.globalAlpha = 1;Un detalle de la especificación: asignar a globalAlpha un valor fuera del rango de 0 a 1, o no finito, se ignora en silencio y la propiedad conserva el valor anterior. Es la misma trampa que en el resto de la API, y produce el bug de un elemento que aparece con la opacidad de otro.
La composición afecta a todo el canvas
Esta es la parte que sorprende y que hay que interiorizar antes de tocar los operadores exóticos.
Un operador de composición no se aplica solo al rectángulo que ocupa la forma que dibujas: se aplica a todo el canvas. La forma define el origen, y el origen en las zonas donde no hay forma es transparente, pero eso no significa “no hacer nada”: significa “componer con un origen transparente”.
Con source-over, componer con origen transparente deja el destino intacto, así que el efecto es indistinguible de no hacer nada. Con otros operadores, no.
ctx.fillStyle = '#a6e3a1';ctx.fillRect(0, 0, 300, 200); // fondo verdectx.globalCompositeOperation = 'source-in';ctx.fillStyle = '#f38ba8';ctx.fillRect(50, 50, 100, 100);// Resultado: SOLO queda el rectangulo rosa. Todo el verde ha desaparecido,// porque source-in conserva el destino solo donde hay origen.Es el comportamiento correcto según la definición del operador y es la causa número uno de “mi canvas se ha borrado”. La defensa es limitar el alcance de la operación con un recorte, o —mucho más habitual— hacer la composición en un canvas auxiliar y traer el resultado.
Restaurar el operador
globalCompositeOperation es estado y persiste. Olvidar restaurarlo produce efectos desconcertantes muy lejos de la causa.
function conComposicion(ctx, modo, dibujar) { const anterior = ctx.globalCompositeOperation; ctx.globalCompositeOperation = modo; try { dibujar(); } finally { ctx.globalCompositeOperation = anterior; }}O, si ya estás usando la pila para otra cosa, save y restore lo guardan igual que al resto del estado.
Un ejemplo del modelo completo
<!doctype html><meta charset="utf-8"><canvas id="c" width="560" height="200" style="background:#181825"></canvas><script> const ctx = document.getElementById('c').getContext('2d'); function pareja(x, modo, etiqueta) { ctx.save(); ctx.translate(x, 20); // El "destino": un cuadrado azul ctx.globalCompositeOperation = 'source-over'; ctx.fillStyle = '#89b4fa'; ctx.fillRect(0, 0, 80, 80); // El "origen": un circulo rosa, compuesto con el modo indicado ctx.globalCompositeOperation = modo; ctx.fillStyle = '#f38ba8'; ctx.beginPath(); ctx.arc(80, 80, 42, 0, Math.PI * 2); ctx.fill(); ctx.globalCompositeOperation = 'source-over'; ctx.fillStyle = '#a6adc8'; ctx.font = '12px system-ui'; ctx.fillText(etiqueta, 0, 140); ctx.restore(); } // Cada pareja en su propio canvas conceptual gracias al clip ['source-over', 'source-atop', 'destination-over', 'xor'].forEach((m, i) => { ctx.save(); ctx.beginPath(); ctx.rect(20 + i * 135, 10, 125, 180); ctx.clip(); // limita el alcance de la composicion pareja(20 + i * 135, m, m); ctx.restore(); });</script>El clip alrededor de cada pareja es lo que permite mostrar cuatro operadores en el mismo canvas: sin él, cada operador afectaría a todo el lienzo y se destruirían entre sí. Es la primera aplicación práctica de la interacción entre recorte y composición, que tiene su propia lección.