Las unidades de recorte y máscara
clipPathUnits, maskUnits y maskContentUnits: tres atributos con valores por omisión distintos, la región del 120 por ciento, y por qué una máscara recorta lo que no debería.
Recortes y máscaras arrastran el mismo problema de coordenadas que los gradientes, con un agravante: hay tres atributos en juego y sus valores por omisión no son coherentes entre sí. clipPathUnits empieza en espacio de usuario, maskUnits en caja envolvente y maskContentUnits otra vez en espacio de usuario. Saber cuál es cuál explica de golpe los dos síntomas más frecuentes: el recorte que aparece minúsculo en una esquina, y la máscara que corta lo que debería dejar pasar.
- Enumerar los tres atributos de unidades, su valor por omisión y qué controla cada uno.
- Convertir un recorte de espacio de usuario a caja envolvente para reutilizarlo entre formas.
- Ampliar la región de una máscara para que no corte el contenido que desborda.
- Reconocer los dos síntomas característicos de unidades mal elegidas.
La tabla
| Atributo | Elemento | Valor por omisión | Controla |
|---|---|---|---|
clipPathUnits |
clipPath |
userSpaceOnUse |
Las coordenadas de las formas del recorte |
maskUnits |
mask |
objectBoundingBox |
x, y, width, height del mask |
maskContentUnits |
mask |
userSpaceOnUse |
Las coordenadas del contenido del mask |
Tres observaciones inmediatas.
clipPath tiene un solo atributo de unidades, porque no tiene región: la región de un recorte es la unión de sus formas. mask tiene dos, porque sí tiene región, y esa es la fuente de la mitad de sus problemas.
Los valores por omisión de clipPathUnits y maskContentUnits coinciden: espacio de usuario. Es decir, por omisión, tanto el contenido de un recorte como el de una máscara se dibujan en coordenadas absolutas del lienzo. Eso es lo intuitivo y lo que casi nadie cambia.
El que sale del patrón es maskUnits, que por omisión es caja envolvente, y controla algo que ni siquiera es visible: la extensión de la región donde la máscara tiene efecto.
El recorte reutilizable
Con clipPathUnits="userSpaceOnUse", las formas del recorte están donde están. Si el elemento recortado se mueve, el recorte no lo sigue: la parte visible es otra.
Para un recorte que se adapte a cualquier forma, hay que pasarlo a caja envolvente. Entonces las coordenadas son fracciones de 0 a 1 de la caja del elemento recortado.
<svg viewBox="0 0 240 100" width="480">
<defs>
<!-- Un triangulo definido en el cuadrado unitario -->
<clipPath id="tri" clipPathUnits="objectBoundingBox">
<polygon points="0.5,0 1,1 0,1" />
</clipPath>
</defs>
<rect x="10" y="10" width="80" height="80" fill="#89b4fa" clip-path="url(#tri)" />
<rect x="120" y="30" width="110" height="40" fill="#a6e3a1" clip-path="url(#tri)" />
</svg>
El mismo recorte produce un triángulo en cada forma, adaptado a su tamaño y posición. Sin clipPathUnits="objectBoundingBox" habría que escribir un clipPath por elemento.
El precio es el conocido: en una caja no cuadrada, el triángulo se deforma. Y la caja degenerada tiene el mismo efecto que en los gradientes: con objectBoundingBox y una caja de altura o anchura cero, el recorte no se aplica. En ese caso el elemento se dibuja sin recortar, no desaparece; es un comportamiento distinto del de los gradientes y conviene saberlo.
Un detalle práctico: cuando clipPathUnits es objectBoundingBox, escribir las coordenadas como decimales entre 0 y 1 es lo habitual, pero los porcentajes también valen y se leen mejor. polygon points="50%,0 100%,100% 0,100%" es exactamente lo mismo.
La región de la máscara
Este es el apartado que resuelve el bug. Un mask tiene una región rectangular, definida por x, y, width y height, y fuera de esa región el valor de la máscara es cero, es decir, todo se oculta.
Los valores por omisión, en objectBoundingBox, son:
x="-10%" y="-10%" width="120%" height="120%"
Es decir, la caja envolvente del elemento ampliada un diez por ciento por cada lado. Ese margen del diez por ciento es una concesión, y es insuficiente en cuanto el elemento pinta fuera de su caja geométrica, que ocurre con:
- un
strokegrueso, que sobresale medio grosor; - un
filterque desplaza o desenfoca; - marcadores en los vértices;
- un
textcuyos descendentes bajan más de lo previsto.
El síntoma: aplicas una máscara blanca (que en teoría no oculta nada) y el elemento aparece cortado por un rectángulo invisible. La causa no es la máscara, es su región.
<defs>
<!-- Region ampliada para que no corte -->
<mask id="m" maskUnits="objectBoundingBox"
x="-50%" y="-50%" width="200%" height="200%">
<rect x="-1000" y="-1000" width="3000" height="3000" fill="#fff" />
<circle cx="100" cy="50" r="30" fill="#000" />
</mask>
</defs>
Dos cosas ahí. La región se amplía al doble, con margen de sobra. Y el rectángulo blanco del contenido se hace mucho mayor que el elemento, porque si el contenido de la máscara no cubre una zona, esa zona vale cero igualmente.
Para casos donde la geometría es conocida, la alternativa más limpia es pasar la región a espacio de usuario y darle coordenadas explícitas del viewBox:
<mask id="m2" maskUnits="userSpaceOnUse" x="0" y="0" width="200" height="100">
<rect width="200" height="100" fill="#fff" />
<circle cx="100" cy="50" r="30" fill="#000" />
</mask>
Así la región es todo el lienzo y no hay margen de error.
Hay dos cuadros clínicos, y confundirlos hace perder mucho tiempo porque los arreglos son opuestos.
Síntoma A: el recorte o la máscara aparece diminuto en la esquina superior izquierda, o el elemento desaparece casi entero. Causa: has escrito coordenadas en unidades de usuario (números como 100, 240) en un contexto que las interpreta como fracciones de la caja. Un cx="100" en objectBoundingBox significa cien veces el ancho de la forma. El diagnóstico definitivo: si los números de tu recorte son mayores que 1, y el atributo de unidades correspondiente dice objectBoundingBox, ese es el problema. Arreglo: cambia las unidades o divide los números.
Síntoma B: la máscara corta el contenido con un borde recto que no está en ninguna parte del marcado. Causa: la región de la máscara. El borde recto es el rectángulo x/y/width/height del mask. Arreglo: amplía la región.
La forma de distinguirlos en un vistazo: en el síntoma A el corte sigue la forma del recorte, en el B el corte es un rectángulo perfecto que no se corresponde con nada. Un rectángulo perfecto e inexplicable en un efecto de máscara o filtro es siempre una región, nunca un error de geometría.
Y una consecuencia que cierra el círculo con el nivel 20: exactamente el mismo síntoma B, con exactamente la misma causa, es el que produce un filter cuya sombra sale cortada. La región por omisión de filter es la misma: menos diez por ciento y ciento veinte por ciento. Cuando aprendas a reconocer el rectángulo, lo reconocerás en los tres sitios.
Elegir las unidades
El criterio, en tres líneas.
Recorte específico de un elemento: clipPathUnits="userSpaceOnUse" (el valor por omisión) y coordenadas del lienzo. Simple y explícito.
Recorte reutilizable entre formas de tamaños distintos: clipPathUnits="objectBoundingBox" y coordenadas de 0 a 1 o porcentajes. Acepta la deformación como parte del trato.
Máscara: maskUnits="userSpaceOnUse" con una región que cubra todo lo que pueda pintar el elemento, y el contenido en coordenadas del lienzo. Es la configuración que no sorprende, y la que hay que escribir por defecto hasta que haya una razón para otra cosa.
Coge un elemento con stroke-width="20" y aplícale una máscara blanca completa. Comprueba que el trazo aparece cortado. Después arréglalo de las dos maneras (ampliando la región relativa y pasando la región a espacio de usuario) y verifica que las dos funcionan. Repite el experimento con un filter de sombra y confirma que el rectángulo de corte es el mismo fenómeno.