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