wandres.dev
FILTROS Y FUSIÓN · filter, backdrop-filter, blend modes

Las diez funciones de filtro y qué hace cada una por dentro

La operación exacta que ejecuta cada función, qué valores admite más allá de 1, por qué hue-rotate no rota el tono que crees, y cuándo hace falta un filtro de SVG.

⏱ 18 min

Las funciones de filter son atajos para primitivas de filtro de SVG, y cada una tiene una definición matemática concreta que explica su comportamiento en los extremos. Saber que contrast() es una recta con una intersección calculada, o que hue-rotate() es una matriz lineal y no una rotación de tono de verdad, evita las sorpresas que aparecen en cuanto sales del rango cómodo.

🎯 Al terminar esta lección sabrás
  • Enumerar las diez funciones y el tipo de operación de cada una.
  • Saber cuáles admiten valores por encima de 1 y qué producen.
  • Explicar por qué hue-rotate() no conserva la claridad.
  • Reconocer cuándo el atajo de CSS no llega y hace falta url().

Las que interpolan hacia un extremo

Cuatro funciones toman una cantidad entre 0 y 1 e interpolan entre “la imagen original” y “la imagen totalmente transformada”. Valores por encima de 1 se recortan a 1.

Función Con 0 Con 1
grayscale() Original Escala de grises completa
sepia() Original Tono sepia completo
invert() Original Negativo completo
opacity() Transparente Original

grayscale() y sepia() son matrices de color interpoladas hacia la matriz de luminancia y hacia la matriz sepia respectivamente. invert() es una tabla de transferencia que va de amount a 1 - amount en cada canal, con lo que invert(0.5) produce un gris uniforme: cada canal se mapea a 0.5 sea cual sea su valor.

opacity() merece una nota. Hace lo mismo que la propiedad opacity con una diferencia importante de posición en el pipeline: como filtro se aplica dentro de la cadena, así que un drop-shadow() posterior verá el elemento ya semitransparente y proyectará una sombra atenuada. Con la propiedad opacity, la atenuación se aplica al final, sobre el resultado ya filtrado, sombra incluida.

Las que escalan sin techo

Tres funciones aceptan valores por encima de 1 y no se recortan.

brightness(k) multiplica cada canal por k. Con k mayor que 1 aclara y satura hacia el blanco; los canales que superen 1 se recortan al escribir. Con k igual a 0 da negro.

contrast(k) aplica a cada canal la recta salida = k * entrada + (0.5 - 0.5 * k). La intersección está calculada para que el punto medio, 0.5, se quede fijo: contrast() gira la curva de transferencia alrededor del gris medio. Con k igual a 0 todo colapsa a gris medio; con k muy grande la imagen se convierte en dos tonos.

saturate(k) aplica la matriz de saturación de SVG, que interpola entre la luminancia y el color original y extrapola más allá cuando k es mayor que 1. Con k igual a 0 equivale a grayscale(1).

De ahí sale un truco conocido y de verdad útil: la combinación de blur() y contrast() produce fusión de formas, el llamado efecto de metabolas. El desenfoque convierte los bordes duros en rampas de alfa; el contraste extremo vuelve a endurecer esas rampas, pero las de dos formas cercanas ya se han sumado, con lo que se unen en una sola silueta.

.gotas {
  filter: blur(12px) contrast(24);
  background: #11111b;
}

El fondo opaco del contenedor es necesario: el contraste actúa sobre el alfa compuesto, y sin un fondo el resultado es distinto.

hue-rotate y su trampa

hue-rotate(<angle>) está definida como una matriz de color de 5 por 4 con coeficientes que dependen del seno y el coseno del ángulo. Es una rotación en un espacio lineal construido alrededor del eje de luminancia, y es una aproximación barata que se diseñó en los años noventa para hardware de la época.

Lo que no es: una rotación del tono en ningún espacio perceptual. Consecuencias concretas:

No conserva la claridad. Rotar 180 grados un azul y un amarillo no produce el intercambio limpio que esperarías; unos colores salen más claros y otros más oscuros.

No conserva la saturación. Ciertos ángulos comprimen el croma y otros lo expanden.

Puede salirse del rango. La matriz produce valores negativos o mayores que 1, que se recortan, y ese recorte cambia el tono resultante.

Si lo que quieres es de verdad girar el tono, la herramienta correcta no es este filtro sino la sintaxis de color relativo, que opera sobre el tono de OKLCH y sí lo conserva todo lo demás:

/* Aproximado y con derivas. */
.a { filter: hue-rotate(60deg); }

/* Exacto, si el color es tuyo y no una imagen. */
.b { color: oklch(from var(--base) l c calc(h + 60)); }

La diferencia es que el filtro funciona sobre imágenes y fotografías, donde no hay ningún color declarado que puedas derivar, y ahí sigue siendo la única opción. Para colores de la interfaz, no lo uses.

drop-shadow y url

drop-shadow() ya tiene su sitio en la lección sobre las dos sombras de CSS y aquí solo importa recordar dos cosas: que se encadena como cualquier otra función y por tanto la segunda ve la salida de la primera, y que su radio de desenfoque es la mitad del sigma de blur().

url() referencia un elemento <filter> de SVG y es la vía de escape cuando los atajos no llegan:

.textura { filter: url(#mi-filtro); }

Hace falta cuando necesitas algo que no está en el catálogo: una matriz de color arbitraria con feColorMatrix, ruido con feTurbulence, desplazamiento con feDisplacementMap, iluminación con feDiffuseLighting, o cualquier composición de varias primitivas con sus propios canales. También cuando necesitas controlar el espacio de color de la operación, cosa que las funciones de CSS no permiten.

El coste de esa flexibilidad es que el SVG tiene que existir en el documento y que la referencia es por identificador, con todos los problemas que eso trae en componentes que se mueven entre documentos o dentro de un shadow root.

ℹ️
Las unidades de longitud solo aparecen en dos funciones

blur() y drop-shadow() son las únicas que toman longitudes; el resto toma números o porcentajes, que son intercambiables: saturate(1.5) y saturate(150%) son idénticos. Y las longitudes de blur() y drop-shadow() no admiten porcentajes, así que un desenfoque no puede ser proporcional al tamaño del elemento sin pasar por una custom property y calc().

Estos filtros son matrices de los noventa, y por eso no se comportan como los de tu editor de fotos

Merece la pena saber de dónde vienen estas funciones, porque explica por qué producen resultados que en un editor de imágenes serían inaceptables. Las primitivas de filtro de SVG se especificaron a finales de los noventa con un objetivo declarado: que se pudieran implementar con operaciones baratas sobre valores de ocho bits, en el hardware de la época y sin coma flotante. De ahí que casi todas sean matrices de color de cuatro por cinco o tablas de transferencia por canal: dos cosas que un procesador de entonces podía hacer a velocidad de vídeo. hue-rotate es el ejemplo más claro del compromiso: una rotación de tono correcta requiere convertir a un espacio polar, girar el ángulo y volver, lo cual son raíces cuadradas y arcotangentes por píxel; lo que la especificación define en su lugar es una única matriz de tres por tres cuyos nueve coeficientes se calculan con un seno y un coseno del ángulo, aplicada en RGB. Es rapidísima, y no es una rotación de tono: es una rotación en un espacio que no es perceptualmente uniforme y ni siquiera es lineal en luz, porque opera sobre valores con gamma. Lo mismo vale para saturate, que interpola contra una luminancia calculada con los coeficientes de la televisión analógica —0.213, 0.715 y 0.072— sobre valores codificados, cosa que ninguna definición moderna de saturación haría. La conclusión operativa no es dejar de usarlos, porque son baratos, están en todas partes y para muchos efectos el resultado es perfectamente aceptable. La conclusión es no esperar de ellos precisión colorimétrica y no construir un sistema de color encima. Un filter: brightness(1.1) sobre un botón para hacer un :hover funciona; el mismo filtro aplicado sistemáticamente a toda una paleta produce derivas de tono y de contraste que no controlas y que no puedes medir, porque el resultado no está en ninguna custom property: está en los píxeles. Para el color de la interfaz, deriva en OKLCH; para tratar imágenes, filtros.

⚔️ Explora los extremos
  1. Aplica invert(0.5) a una foto y comprueba que sale gris uniforme.
  2. Encuentra el valor de contrast() con el que una foto se convierte en dos tonos.
  3. Monta el efecto de metabolas con blur() y contrast() sobre dos círculos que se acercan.
  4. Compara hue-rotate(120deg) con la rotación equivalente en oklch sobre un color plano.
  5. Escribe un feColorMatrix a mano y referéncialo con url().