El pipeline de filter y por qué el orden importa
Qué recibe exactamente un filtro como entrada, cómo se encadenan las funciones, en qué espacio de color operan, y los tres efectos secundarios de aplicar cualquier filtro.
filter no es una propiedad que ajuste el aspecto de un elemento: es una cadena de operaciones sobre una imagen, y esa imagen es el elemento y todos sus descendientes ya pintados. Ver la propiedad como un pipeline explica de golpe por qué el orden de las funciones cambia el resultado, por qué un filtro atrapa a los elementos fijos de dentro, y por qué es una de las cosas más caras que se pueden escribir en CSS.
- Describir qué recibe la cadena de filtros como entrada.
- Predecir el resultado de dos cadenas con las mismas funciones en distinto orden.
- Indicar en qué espacio de color operan las funciones de filtro de CSS.
- Enumerar los tres efectos colaterales de un
filterdistinto denone.
La entrada de la cadena
Antes de que se aplique el primer filtro, el navegador tiene que rasterizar el elemento y todo su subárbol a una textura aparte. Eso incluye el fondo, el borde, el texto, las imágenes y todos los descendientes con sus propios efectos ya resueltos.
Sobre esa textura se aplican las funciones de filter en el orden en que las escribiste, cada una tomando como entrada la salida de la anterior. El resultado final es lo que se compone sobre la página.
flowchart TB
A[Elemento y subarbol pintados a una textura] --> B{Cadena de filter}
B --> C[grayscale al cien por cien]
C --> D[sepia al cien por cien]
D --> E[Salida en tono sepia]
B --> F[sepia al cien por cien]
F --> G[grayscale al cien por cien]
G --> H[Salida en gris]
E --> Z[Se compone sobre la pagina]
H --> Z
style A fill:#89b4fa,color:#11111b
style B fill:#cba6f7,color:#11111b
style E fill:#fab387,color:#11111b
style H fill:#f9e2af,color:#11111b
style Z fill:#a6e3a1,color:#11111bLas dos ramas del diagrama son estas dos declaraciones:
.a { filter: grayscale(1) sepia(1); } /* acaba en sepia */
.b { filter: sepia(1) grayscale(1); } /* acaba en gris */
La primera convierte a gris y después tiñe ese gris de sepia: el resultado es sepia. La segunda tiñe de sepia y después desatura ese sepia: el resultado es gris. Mismas funciones, mismos valores, resultados incompatibles.
Ese es el modelo mental que hay que llevarse: filter es una expresión, no un conjunto de ajustes. Cada función recibe la imagen entera y devuelve otra imagen entera.
Los efectos colaterales
Cualquier valor de filter distinto de none provoca tres cosas que no tienen nada que ver con el aspecto.
Crea un contexto de apilamiento. El subárbol se compone como una unidad y ningún z-index de dentro puede salir de él. Un elemento posicionado dentro de un filtrado nunca se pondrá por encima de un hermano del filtrado.
Crea un bloque contenedor para descendientes posicionados. Los position: fixed de dentro dejan de referirse al viewport y pasan a referirse a este elemento; los position: absolute también lo toman como referencia si no hay otro más cercano. Es idéntico a lo que hace transform, y es la explicación de casi todos los “mi cabecera fija se ha quedado dentro de una tarjeta”.
Obliga a rasterizar el subárbol. No hay filtro sin textura intermedia. Con un subárbol grande eso significa memoria de vídeo, un paso de composición extra, y la pérdida de algunas optimizaciones de renderizado de texto que dependen de pintar directamente sobre el destino.
Los tres son inevitables. La única decisión que tienes es a qué elemento le aplicas el filtro, y la respuesta correcta casi siempre es al más pequeño que resuelva el problema.
Para animar hacia un filtro y de vuelta, los dos extremos tienen que tener la misma lista de funciones. filter: none interpola con cualquier cadena tratando cada función como su valor neutro, pero blur(0px) y none no siempre se comportan igual en cuanto a promoción de capa. Si el elemento parpadea al final de la transición, declara el estado inicial con los valores neutros explícitos: filter: blur(0px) saturate(1).
El espacio de color de las operaciones
Un detalle que casi nunca aparece y que explica diferencias entre CSS y SVG que parecen fallos.
Las funciones de filtro de CSS —blur(), brightness(), saturate() y compañía— están definidas para operar en el espacio sRGB, es decir sobre los valores ya codificados con gamma.
Las primitivas de filtro de SVG, en cambio, operan por defecto en linearRGB, sobre valores proporcionales a la luz, y hay que escribir color-interpolation-filters="sRGB" para que se comporten como las de CSS.
La consecuencia es que un filter: blur(8px) de CSS y un <feGaussianBlur stdDeviation="8"> de SVG sin ese atributo no dan el mismo resultado. El de SVG desenfoca en luz lineal, con lo que las zonas claras dominan más la mezcla y el resultado es más luminoso en los bordes; el de CSS mezcla los códigos con gamma y da un resultado más oscuro.
Cuál de los dos es “correcto” depende de qué estés simulando. Un desenfoque óptico real ocurre con luz, es decir en lineal. Un desenfoque de interfaz, que solo tiene que verse bien, casi siempre queda mejor en sRGB porque conserva mejor el contraste percibido. La especificación de CSS eligió sRGB por compatibilidad y porque es lo que la gente esperaba, no porque sea más fiel.
La regla que resuelve la mayoría de las sorpresas con filter es esta: cuando el filtro empieza a trabajar, el documento ya no existe. Lo que hay es un rectángulo de píxeles con canal alfa. Ni elementos, ni texto, ni cajas, ni jerarquía. De ahí se deducen, sin tener que memorizar nada, todas las limitaciones. No hay forma de filtrar solo una parte del elemento, porque no hay partes: hay píxeles. Si quieres desenfocar el fondo y no el texto, tienen que ser dos elementos, porque el filtro no distingue uno de otro dentro del mismo mapa de píxeles. No hay forma de que un filtro afecte a un hijo y no a otro, por la misma razón; para eso hay que aplicarlo al hijo. No hay forma de deshacer un filtro en un descendiente: escribir filter: none en un hijo no lo exime, porque el filtro del padre se aplica sobre el resultado que ya incluye a ese hijo. Y el texto filtrado deja de renderizarse con las optimizaciones de subpíxel, porque esas optimizaciones requieren conocer el color del fondo sobre el que se pinta el glifo, y aquí el glifo se pinta primero a una textura transparente y se compone después; por eso el texto dentro de un elemento con filtro se ve ligeramente más fino o más borroso, incluso con un filtro que no debería tocarlo. La consecuencia arquitectónica, que es lo que hay que llevarse, es que la unidad de filtrado es el elemento y no hay granularidad menor. Diseñar con filtros consiste en decidir dónde poner las fronteras entre elementos para que cada filtro pille exactamente lo que tiene que pillar, y eso a menudo significa marcado adicional que no existiría por ninguna otra razón. Un contenedor extra que solo existe para que el filtro tenga a qué agarrarse no es una chapuza: es la consecuencia directa de que el filtro opera sobre píxeles.
- Aplica
grayscale(1) sepia(1)ysepia(1) grayscale(1)a la misma imagen. - Encuentra otra pareja de funciones cuyo orden cambie el resultado.
- Pon un
position: fixeddentro de un elemento confilter: blur(0.1px)y localiza dónde acaba. - Compara un
blurde CSS con unfeGaussianBlurde SVG sincolor-interpolation-filters. - Intenta desenfocar el fondo de un elemento sin desenfocar su texto usando solo
filter.