mask con imágenes, degradados y composición
Las ocho propiedades de máscara y sus valores iniciales sorprendentes, la diferencia entre modo alfa y modo luminancia, y las cuatro operaciones de mask-composite.
mask es la familia de propiedades que más se parece a background: mismas capas, mismos nombres, misma sintaxis de posición y tamaño. Esa simetría es deliberada y ayuda, salvo en dos puntos donde los valores iniciales no son los que esperas y donde el modo de interpretación cambia según de dónde venga la máscara. Con esas dos cosas claras, se abre la composición por capas, que es lo que de verdad tiene aquí valor.
- Escribir las ocho propiedades de máscara y sus abreviadas.
- Explicar la diferencia entre
alpha,luminanceymatch-source. - Componer varias capas de máscara con las cuatro operaciones de conjunto.
- Construir el borde de degradado con esquinas redondeadas.
La familia y sus valores iniciales
| Propiedad | Valor inicial | Equivalente en fondos |
|---|---|---|
mask-image |
none |
background-image |
mask-position |
center |
background-position |
mask-size |
auto |
background-size |
mask-repeat |
repeat |
background-repeat |
mask-origin |
border-box |
background-origin |
mask-clip |
border-box |
background-clip |
mask-mode |
match-source |
no tiene |
mask-composite |
add |
no tiene |
Dos de esos valores iniciales muerden. mask-repeat vale repeat, igual que en fondos, con lo que un degradado de máscara sin mask-size explícito y con no-repeat puede acabar repitiéndose si le das un tamaño. Y mask-position vale center, no 0% 0% como en fondos, lo cual es una asimetría que sorprende al portar código de una familia a la otra.
mask-origin vale border-box, otra asimetría: en fondos el origen es padding-box. Aquí tiene sentido, porque una máscara suele querer cubrir el elemento entero, borde incluido.
La abreviada acepta todo, con la barra separando posición de tamaño y las palabras de caja en el orden origen y luego recorte:
.a {
mask: url(forma.svg) center / contain no-repeat;
}
.b {
mask: linear-gradient(#000, transparent) 0 0 / 100% 100% no-repeat border-box;
}
Alfa, luminancia y match-source
Una máscara puede interpretarse de dos formas. En modo alfa, lo que importa es el canal alfa de la imagen de máscara: opaco enmascara al 100%, transparente al 0%, y el color se ignora por completo. En modo luminancia, lo que importa es el brillo: blanco enmascara al 100%, negro al 0%, y el alfa se multiplica encima.
mask-mode: match-source es el valor inicial y decide según el origen: para cualquier valor de tipo imagen, incluidos los degradados, el modo es alfa; para una referencia a un elemento <mask> de SVG, luminancia.
Eso explica un montón de confusión. Un degradado de blanco a negro no enmascara nada: los dos extremos son opacos, así que en modo alfa la máscara vale 1 en todas partes. El degradado que enmascara es de un color opaco a transparente:
/* No hace nada: los dos colores son opacos. */
.mal { mask-image: linear-gradient(#fff, #000); }
/* Correcto en modo alfa. */
.bien { mask-image: linear-gradient(#000, transparent); }
/* Correcto si fuerzas luminancia. */
.tambien { mask-image: linear-gradient(#fff, #000); mask-mode: luminance; }
Cuando el degradado se usa en modo alfa, el color es irrelevante: #000, #fff o red producen exactamente el mismo resultado mientras el alfa sea el mismo. Por convención se escribe negro, y da igual.
Forzar luminance tiene un uso concreto: reutilizar como máscara una imagen en escala de grises que no tiene canal alfa, cosa habitual cuando la máscara viene de una textura o de un mapa generado.
Componer varias máscaras
Aquí está lo que hace de mask una herramienta y no un truco. Cada capa de máscara se combina con el resultado acumulado de las anteriores mediante mask-composite, que ofrece cuatro operaciones de conjuntos:
| Valor | Resultado |
|---|---|
add |
Unión. Es el valor inicial |
subtract |
La capa actual menos las anteriores |
intersect |
Intersección |
exclude |
Diferencia simétrica: en una o en otra, pero no en las dos |
Con eso se construyen regiones que ninguna forma describe. El caso canónico es el anillo, y con él el borde de degradado que sigue las esquinas redondeadas, que es la asignatura pendiente de border-image:
.borde-degradado {
position: relative;
border-radius: 16px;
}
.borde-degradado::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
padding: 2px;
background: linear-gradient(in oklch to bottom right, #89b4fa, #f38ba8);
mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
mask-composite: exclude;
pointer-events: none;
}
Merece la pena leerlo despacio porque cada línea hace falta. El pseudoelemento cubre el elemento entero y lleva el degradado de fondo. Tiene dos píxeles de relleno, así que su caja de contenido es el rectángulo interior. La primera capa de máscara es un rectángulo opaco recortado a content-box, es decir, el interior. La segunda es un rectángulo opaco sin recorte, es decir, todo. exclude deja lo que está en una y no en la otra: el anillo de dos píxeles entre ambas. Y como el pseudoelemento hereda el radio, el anillo sigue la curva.
linear-gradient(#000 0 0) es el idioma para “una imagen de un color plano”: un degradado con las dos paradas en la misma posición. No hay una función solid-color() en CSS, así que este es el atajo estándar.
Las versiones antiguas de WebKit implementaron -webkit-mask-composite con las palabras de Porter-Duff: source-over, source-in, source-out y xor. El equivalente de exclude es xor. Si necesitas cubrir esas versiones, declara las dos, y ten en cuenta que el orden de las capas se interpreta al revés en la sintaxis prefijada.
Otros patrones de una línea
Desvanecer el final de un contenedor con desplazamiento:
.desplazable {
mask-image: linear-gradient(to bottom, #000 calc(100% - 48px), transparent);
}
Recortar una imagen con una forma suave sin tocar el archivo:
.retrato {
mask-image: radial-gradient(circle closest-side, #000 70%, transparent);
}
Un icono monocromo coloreable desde CSS, que es la alternativa moderna a las fuentes de iconos:
.icono {
display: inline-block;
inline-size: 1.25em;
block-size: 1.25em;
background: currentColor;
mask: url(iconos/flecha.svg) center / contain no-repeat;
}
El SVG aporta la forma y el color lo pone currentColor, así que el icono sigue al texto en cualquier estado y en cualquier tema sin duplicar archivos.
La forma productiva de pensar en mask no es “recortar con una imagen” sino esta: la máscara es un multiplicador del canal alfa del elemento, y no es el único que hay. opacity es otro. El alfa del propio contenido es otro. El alfa que aporte un filter es otro. Todos se multiplican, y entender que se multiplican en un orden concreto resuelve efectos que de otro modo parecen imposibles y explica resultados que de otro modo parecen fallos. El orden es: primero el elemento y su subárbol se pintan con sus alfas propios, después se aplican los filtros, después la máscara, y por último opacity al componer el resultado sobre lo que hay detrás. De ahí sale que opacity: 0.5 sobre un elemento enmascarado produce medio alfa de la región enmascarada, no una máscara al 50%, y que la diferencia se ve en cuanto hay solapamiento interno: un elemento con hijos que se superponen y opacity: 0.5 se ve uniformemente traslúcido, mientras que los mismos hijos con opacity: 0.5 cada uno se ven acumulados donde se solapan. Y sale también el patrón que muy poca gente usa: como la máscara se aplica antes de opacity, se pueden encadenar dos niveles de transparencia independientes, uno espacial y otro global, y animar solo el segundo. Un desvanecimiento de borde que es fijo y una entrada que es animada no compiten por la misma propiedad: la máscara define dónde y opacity define cuánto, y las dos conviven sin que ninguna tenga que recalcularse. Es la misma separación que hay entre forma y color en cualquier sistema gráfico, y es la razón por la que la máscara es una propiedad de composición y no un simple recorte con degradado.
- Enmascara con
linear-gradient(#fff, #000)y comprueba que no hace nada. Arréglalo de las dos formas. - Monta el borde de degradado con
mask-composite: excludey cámbiale el radio. - Convierte un SVG monocromo en un icono coloreable con
currentColor. - Prueba las cuatro operaciones de composición con dos círculos superpuestos.
- Combina una máscara de desvanecimiento fija con una animación de
opacityy verifica que no interfieren.