clip: el recorte y su interacción con la pila
Usar la región de recorte del canvas, entender por qué solo se puede reducir y nunca ampliar, y combinarla con la composición de forma controlada.
clip convierte la ruta actual en una región fuera de la cual no se pinta nada. Es una operación de una sola dirección: cada llamada intersecta con la región vigente y nunca la amplía, así que la única forma de deshacerla es la pila de estado. Esa asimetría, que parece una limitación arbitraria, es lo que hace que el recorte sea componible y lo que obliga a una disciplina concreta.
- Aplicar una región de recorte con las dos reglas de relleno.
- Explicar por qué
clipsolo intersecta y cuáles son las dos formas de deshacerlo. - Combinar recorte y composición para limitar el alcance de un operador.
- Elegir entre
clipy las alternativas de máscara según el caso.
La operación
ctx.beginPath();
ctx.arc(150, 150, 100, 0, Math.PI * 2);
ctx.clip(); // regla nonzero por defecto
ctx.clip('evenodd'); // o la regla par-impar
ctx.clip(rutaPath2D); // o una Path2D directamente
A partir de la llamada, todo lo que dibujes se recorta a esa región. El recorte respeta el antialiasing: los bordes de la región no son duros, sino que tienen cobertura parcial, lo que da recortes suaves.
clip no consume la ruta: después de recortar, la ruta actual sigue ahí y puedes rellenarla o trazarla.
Y clip usa la matriz de transformación vigente en el momento de construir la ruta, no en el de aplicar el recorte, por la misma razón que el resto de las rutas: los puntos se transforman al añadirlos.
Solo se reduce
Llamar a clip dos veces da la intersección de ambas regiones, nunca la unión.
ctx.beginPath(); ctx.rect(0, 0, 200, 200); ctx.clip();
ctx.beginPath(); ctx.rect(100, 100, 200, 200); ctx.clip();
// La region efectiva es el cuadrado de (100,100) a (200,200)
No hay ningún método para ampliar la región ni para quitarla. Las dos únicas formas de volver atrás son:
restore() desde un save() anterior al recorte. Es la vía normal.
reset(), que restablece todo el contexto y además borra el búfer. Es la vía de emergencia.
De ahí sale la regla que no admite excepciones: todo clip va precedido de un save.
ctx.save();
ctx.beginPath();
ctx.roundRect(20, 20, 200, 140, 16);
ctx.clip();
ctx.drawImage(foto, 0, 0, 240, 180);
ctx.restore(); // sin esto, el recorte es para siempre
Olvidar ese save produce un canvas que deja de pintar la mayor parte de las cosas, y como el síntoma aparece lejos de la causa, es de los errores que más tiempo consumen.
Recortar a la unión de varias formas
Como clip solo intersecta, recortar a la unión de dos formas exige construirlas en una sola ruta antes de recortar:
ctx.save();
ctx.beginPath();
ctx.arc(120, 120, 60, 0, Math.PI * 2);
ctx.moveTo(260, 120);
ctx.arc(200, 120, 60, 0, Math.PI * 2); // segunda subruta en la MISMA ruta
ctx.rect(80, 200, 160, 60); // tercera
ctx.clip(); // recorta a la union de las tres
dibujarFondo(ctx);
ctx.restore();
Con la regla nonzero, las subrutas del mismo sentido se unen; las de sentido contrario producirían agujeros. Con evenodd, las zonas de solape se convierten en agujeros, que casi nunca es lo que se quiere para una unión.
Recorte y composición
Aquí está la interacción que resuelve el mayor problema de los operadores destructivos. Un recorte activo limita el alcance de la composición. Un destination-in con clip activo solo afecta a la región recortada; fuera de ella, el canvas queda intacto.
ctx.save();
ctx.beginPath();
ctx.rect(40, 40, 240, 180);
ctx.clip(); // confina todo lo que sigue
ctx.drawImage(foto, 40, 40, 240, 180);
ctx.globalCompositeOperation = 'destination-in';
const g = ctx.createLinearGradient(0, 40, 0, 220);
g.addColorStop(0, 'rgba(0,0,0,1)');
g.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = g;
ctx.fillRect(40, 40, 240, 180);
ctx.restore(); // recupera el operador Y el recorte
Ese patrón evita el canvas auxiliar y es más barato. Su límite es que el recorte tiene bordes duros de forma efectiva para la composición: si el destino tenía contenido justo fuera del rectángulo de recorte, ese contenido no participa y puede haber un corte visible. Cuando el efecto tiene que ser suave en los bordes, la capa auxiliar sigue siendo lo correcto.
Y clearRect también respeta el recorte, lo que hace que la rutina de limpieza de un canvas tenga que quitar el recorte antes:
function limpiarTodo(ctx) {
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0);
// Si hay un recorte activo, este clearRect no lo salta.
// La unica garantia es que el save/restore de fuera lo haya quitado.
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.restore();
}
Los recortes tienen un coste que varía enormemente según su forma, y esa variación no está documentada en ninguna parte. Un recorte rectangular alineado con los ejes es prácticamente gratis: el navegador lo traduce a una tijera del hardware, que es una funcionalidad de la GPU con coste cero. Un recorte de cualquier otra forma —un círculo, una ruta compleja, un rectángulo rotado— exige generar una máscara de cobertura, que es un búfer adicional del tamaño de la región, rellenarlo rasterizando la ruta, y aplicarlo en cada operación de dibujo posterior. En algunos motores y en algunas combinaciones eso obliga además a caer al camino de software para todo el bloque recortado. Los síntomas son característicos: un cambio de radio de esquina de 0 a 8 píxeles multiplica por cinco el tiempo de un fotograma; una lista con cien tarjetas recortadas va a la mitad de velocidad que la misma lista sin recorte; el perfil muestra tiempo en rasterización que no se corresponde con lo dibujado. Las tres defensas. Primera: prefiere rectángulos. Si el recorte solo sirve para limitar el área de dibujo, un rectángulo hace el trabajo. Segunda: recorta una vez y dibuja mucho dentro, en lugar de un recorte por elemento; el coste está en establecer la máscara, no en usarla. Tercera, y la que más veces resuelve el problema: para esquinas redondeadas en elementos repetidos, no uses clip. Rasteriza la forma recortada una sola vez en un canvas de caché y dibújala con drawImage, que es una copia sin máscara. Cien tarjetas con esquinas redondeadas son cien drawImage desde una caché, no cien recortes. La diferencia en una lista larga es de un orden de magnitud.
clip frente a las alternativas
Hay tres formas de conseguir un recorte y cada una tiene su sitio.
clip es la más directa. Su alcance está bien delimitado por la pila, es componible y no necesita canvas auxiliares. Su pega es el coste con formas no rectangulares y que no admite máscaras con transparencia parcial: una región está dentro o fuera, aunque el borde tenga antialiasing.
destination-in en una capa admite máscaras con degradado, lo que permite desvanecidos y transparencias graduales. Su pega es que necesita una capa y que afecta a todo el canvas si no la usas.
Recortar en el origen con drawImage es la más barata cuando la forma es un rectángulo y lo que recortas es una imagen: la firma de nueve parámetros ya hace el recorte sin ninguna máscara.
| Necesidad | Herramienta |
|---|---|
| Limitar el área de dibujo a un rectángulo | clip con rect, o nada si puedes acotar las coordenadas |
| Foto dentro de una forma con borde duro | clip con la ruta |
| Foto que se desvanece hacia un borde | destination-in con degradado, en capa |
| Un trozo de una imagen | drawImage con nueve parámetros |
| Muchos elementos con la misma forma recortada | Canvas de caché y drawImage |