wandres.dev
RECORTE Y MÁSCARA · clipPath y mask en SVG

mask: alfa y luminancia

Las máscaras trabajan con valores continuos, y por defecto los sacan de la luminancia y no del alfa. Qué significa eso, cómo se cambia con mask-type, y el desvanecimiento que solo la máscara permite.

⏱ 17 min

Donde el recorte responde sí o no, la máscara responde con un número entre cero y uno. Ese cambio de dominio lo permite todo: desvanecidos, viñetas, transiciones suaves, texturas de transparencia. Y trae una decisión que SVG resolvió de una manera que sorprende a todo el que viene de CSS: la máscara por omisión no mira el canal alfa, mira el brillo del color. Un negro opaco oculta y un blanco opaco muestra, aunque los dos sean igual de opacos.

🎯 Al terminar esta lección sabrás
  • Escribir una mask y explicar cómo se calcula el valor de máscara de cada píxel.
  • Distinguir el modo luminancia del modo alfa y cambiar entre ellos con mask-type.
  • Construir un desvanecimiento con un gradiente como máscara.
  • Anticipar el efecto de un color de máscara concreto sobre el resultado.

Luminancia por omisión

Un elemento mask contiene marcado que se renderiza en un búfer aparte. De ese búfer se saca, para cada píxel, un valor entre 0 y 1 que multiplica la opacidad del elemento enmascarado.

Cómo se saca ese valor lo decide mask-type, cuyo valor inicial en SVG es luminance. En ese modo, el valor de la máscara es la luminancia del color multiplicada por su alfa. Es decir: blanco opaco da 1, negro opaco da 0, y un gris medio da algo intermedio. El alfa también cuenta: un blanco al 50 por ciento da 0,5.

<svg viewBox="0 0 200 100" width="400">
  <defs>
    <mask id="m">
      <rect width="200" height="100" fill="#000" />
      <circle cx="100" cy="50" r="40" fill="#fff" />
    </mask>
  </defs>
  <rect width="200" height="100" fill="#89b4fa" mask="url(#m)" />
</svg>

Solo se ve el círculo. El rectángulo negro de fondo de la máscara es lo que oculta el resto; sin él, el fondo de la máscara es transparente, cuya luminancia efectiva es cero, y el resultado sería el mismo. Pero conviene ponerlo explícito porque deja clara la intención y porque en modo alfa el comportamiento sí cambia.

La consecuencia práctica de la luminancia: los colores importan y no solo la opacidad. Un #ff0000 opaco tiene una luminancia de aproximadamente 0,21 en el cálculo estándar, así que como máscara oculta casi todo. Un #00ff00 tiene 0,72 y deja pasar casi todo. Si construyes una máscara con la ilustración a color, el resultado es una transparencia que sigue el brillo de los colores, y eso rara vez es lo que querías.

Regla: dentro de una máscara en modo luminancia, usa solo blanco, negro y grises. Cualquier color introduce una variable que no controlas.

mask-type y el modo alfa

mask-type es una propiedad CSS que se aplica al elemento mask, no al elemento enmascarado. Acepta luminance (inicial) y alpha.

<mask id="ma" style="mask-type: alpha">
  <circle cx="100" cy="50" r="40" fill="#000" />
</mask>

En modo alfa, el valor de la máscara es directamente el canal alfa. El color deja de importar: un círculo negro opaco muestra lo que hay debajo exactamente igual que uno blanco opaco.

Cuándo usar cada uno:

Luminancia cuando la máscara es un degradado en escala de grises, una textura o una imagen. Es el modo pensado para eso, y es el que da control fino con un solo canal.

Alfa cuando la máscara son formas dibujadas. Es más intuitivo (pintar una forma la hace visible, sin importar el color), evita el error del color, y es lo que hacen las herramientas de diseño. Si vienes de Figma o de un editor de imagen, mask-type: alpha es el modo que reconoces.

Hay una tercera pieza en el juego: la propiedad mask-mode, que se aplica al elemento enmascarado y permite sobrescribir el modo para esa aplicación concreta. Su soporte ha sido desigual entre motores, así que la vía fiable es mask-type en el propio mask, que es donde el modo pertenece de todas formas.

El desvanecimiento

El uso más frecuente de la máscara, y el que no tiene alternativa con recorte, es el desvanecimiento de un borde:

<svg viewBox="0 0 220 90" width="440">
  <defs>
    <linearGradient id="fade" x1="0" y1="0" x2="1" y2="0">
      <stop offset="0"    stop-color="#fff" />
      <stop offset="0.7"  stop-color="#fff" />
      <stop offset="1"    stop-color="#000" />
    </linearGradient>
    <mask id="mfade">
      <rect width="220" height="90" fill="url(#fade)" />
    </mask>
  </defs>
  <g mask="url(#mfade)">
    <rect width="220" height="90" fill="#313244" />
    <text x="12" y="55" font-size="28" fill="#cdd6f4"
          font-family="system-ui, sans-serif">Texto que se desvanece</text>
  </g>
</svg>

El grupo entero se desvanece hacia la derecha. Fíjate en que la máscara se aplica al g: si se aplicara a cada hijo por separado, el fondo y el texto se desvanecerían de forma independiente y el resultado sería otro.

El mismo patrón en CSS se escribe con mask-image: linear-gradient(...) y es una de las cosas que más se usan de todo este nivel: la lista con scroll que se difumina en los bordes, la marquesina que entra y sale, el texto truncado con degradado en lugar de puntos suspensivos.

Una precisión sobre el degradado de la máscara: como la máscara está en modo luminancia, la parada final tiene que ser negro, no transparente. Si pones transparent, funciona igual (luminancia cero, alfa cero, producto cero) pero la zona intermedia arrastra el problema de la interpolación hacia negro que se explicó en el nivel 20, y el desvanecimiento no es lineal. Con negro explícito, la transición es la que dice el gradiente.

La máscara crea un búfer y un grupo de aislamiento, y eso cambia el resultado de las opacidades

Aplicar una mask obliga al motor a rasterizar el elemento en un búfer aparte, calcular la máscara en otro, multiplicar y componer. Eso tiene tres consecuencias que no se anuncian.

La primera es de rendimiento. Un elemento enmascarado se sale de la ruta rápida de composición: cada fotograma de una animación sobre un elemento enmascarado implica dos rasterizados y una multiplicación por píxel. En un elemento grande y animado es la diferencia entre fluido y entrecortado. La máscara estática sobre contenido estático se cachea y no cuesta nada.

La segunda es de composición. El elemento enmascarado se convierte en un grupo aislado: sus hijos se componen entre sí primero y el resultado se compone con el fondo después. Eso cambia el resultado de mix-blend-mode en los hijos, que dejan de ver lo que hay detrás del grupo. Es el mismo aislamiento que provocan opacity menor que 1, filter y isolation: isolate, y es la causa habitual de que un modo de fusión «deje de funcionar» al añadir una máscara en algún ancestro.

La tercera es la que produce el bug más difícil: el búfer tiene un tamaño finito, determinado por maskUnits y la región de la máscara. Todo lo que caiga fuera de esa región tiene valor de máscara cero, es decir, se oculta. Un elemento que sobresale de su caja envolvente (por un filtro, por un trazo grueso, por un marcador) se corta al aplicarle una máscara aunque la máscara sea blanca por todas partes. El síntoma es que la máscara «recorta», y la causa es que la región por omisión solo cubre la caja envolvente ampliada un diez por ciento por lado. Se arregla ampliando x, y, width y height del mask, y ese es el tema de la lección siguiente.

Máscaras con textura

Como el contenido de la máscara es marcado SVG completo, dentro puede ir cualquier cosa: una imagen, un patrón, un filtro de ruido.

<defs>
  <filter id="ruido">
    <feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="3" />
    <feColorMatrix type="saturate" values="0" />
  </filter>
  <mask id="desgaste">
    <rect width="200" height="100" fill="#fff" />
    <rect width="200" height="100" filter="url(#ruido)" style="mix-blend-mode: multiply" />
  </mask>
</defs>

Un ruido fractal desaturado multiplicado sobre blanco produce una máscara moteada, y el elemento enmascarado sale con aspecto de tinta gastada. Las primitivas de filtro que aparecen ahí son el tema del nivel 22.

⚔️ Reto práctico

Construye la misma máscara circular de dos formas: con un círculo blanco sobre fondo negro en modo luminancia, y con un círculo de cualquier color en modo alfa. Después cambia el color del círculo en las dos versiones y comprueba cuál se ve afectada. Por último, aplica la máscara a un elemento con un stroke grueso y observa el recorte del que habla el callout.