Saltar al contenido

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.

HTML
<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:

Texto
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 stroke grueso, que sobresale medio grosor;
  • un filter que desplaza o desenfoca;
  • marcadores en los vértices;
  • un text cuyos 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.

HTML
<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:

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