wandres.dev
FILTROS SVG · El pipeline de primitivas

Desenfoque, sombra y brillo: efectos completos

feGaussianBlur, feOffset, feFlood, feMerge y feDropShadow combinados en cinco efectos que se pegan y funcionan: sombra exterior, sombra interior, resplandor, sombra larga y borde suave.

⏱ 20 min

Las primitivas de desenfoque y desplazamiento son cuatro y por sí solas no hacen nada interesante. Su valor está en las combinaciones, y esas combinaciones son recetas concretas que llevan veinte años circulando mal copiadas. Aquí están cinco completas, con la región dimensionada y el espacio de color fijado, listas para pegar y con la explicación de por qué cada primitiva está donde está.

🎯 Al terminar esta lección sabrás
  • Usar feGaussianBlur, feOffset, feFlood y feMerge con sus atributos completos.
  • Construir una sombra interior, que ninguna primitiva ofrece de forma directa.
  • Explicar qué hace feDropShadow por dentro y cuándo no basta.
  • Ajustar la relación entre desviación típica y radio percibido de desenfoque.

feGaussianBlur y la desviación típica

feGaussianBlur toma stdDeviation, que es la desviación típica de la gaussiana, no un radio. Acepta un valor (isotrópico) o dos (stdDeviation="6 2" desenfoca más en horizontal).

La correspondencia con el «radio de desenfoque» que usan las herramientas de diseño no es uno a uno. Una gaussiana tiene soporte infinito, pero el 99,7 por ciento de su masa cae dentro de tres desviaciones típicas. En la práctica:

alcance visible ≈ 3 * stdDeviation

Un stdDeviation="4" se extiende unas 12 unidades. Ese es el número que hay que usar para dimensionar la región del filtro y para comparar con el blur-radius de una sombra de CSS, que se define de otra forma: en CSS, el radio de desenfoque equivale aproximadamente al doble de la desviación típica.

stdDeviation ≈ blur-radius de CSS / 2

Un box-shadow: 0 4px 12px se replica con stdDeviation="6", no con 12.

edgeMode controla qué pasa en el borde de la región: duplicate repite el píxel del borde, wrap envuelve, none (el valor por omisión) trata el exterior como transparente. Para un desenfoque sobre una imagen que llena su región, duplicate evita el halo transparente en los bordes.

Efecto 1: sombra exterior

feDropShadow es la abreviatura que hace el trabajo en una línea. Por dentro equivale a una cadena de cuatro primitivas: feGaussianBlur sobre SourceAlpha, feOffset, feFlood con flood-color, feComposite in, y un feMerge con el original encima.

<filter id="sombra" x="-40%" y="-40%" width="180%" height="180%"
        color-interpolation-filters="sRGB">
  <feDropShadow dx="0" dy="6" stdDeviation="8"
                flood-color="#11111b" flood-opacity="0.45" />
</filter>

Úsala siempre que puedas: es más corta, la implementación está optimizada, y es la única de este artículo que no necesita explicación.

Cuándo no basta: cuando quieres varias sombras apiladas, o cuando la sombra tiene que salir de otra cosa que no sea el alfa del elemento. Para varias sombras se encadenan varios feDropShadow, pero cuidado: el segundo toma como entrada el resultado del primero, así que la segunda sombra sombrea también a la primera. Para sombras independientes hay que hacerlo a mano:

<filter id="dos-sombras" x="-50%" y="-50%" width="200%" height="200%"
        color-interpolation-filters="sRGB">
  <feDropShadow in="SourceGraphic" dx="0" dy="2"  stdDeviation="2"
                flood-color="#11111b" flood-opacity="0.35" result="s1" />
  <feDropShadow in="SourceGraphic" dx="0" dy="12" stdDeviation="10"
                flood-color="#11111b" flood-opacity="0.25" result="s2" />
  <feMerge>
    <feMergeNode in="s2" />
    <feMergeNode in="s1" />
    <feMergeNode in="SourceGraphic" />
  </feMerge>
</filter>

Cada feDropShadow parte de SourceGraphic explícitamente, y el feMerge las apila en el orden correcto. Es la traducción exacta de un box-shadow con dos capas.

Efecto 2: sombra interior

No hay primitiva para esto y la construcción es instructiva. La idea: coger el alfa del elemento, desplazarlo y desenfocarlo, restarlo del alfa original para quedarse con el borde interior, teñirlo, y ponerlo encima.

<filter id="interior" x="-20%" y="-20%" width="140%" height="140%"
        color-interpolation-filters="sRGB">
  <!-- 1. Silueta desplazada y desenfocada -->
  <feOffset in="SourceAlpha" dx="0" dy="4" result="desplazado" />
  <feGaussianBlur in="desplazado" stdDeviation="4" result="difuso" />
  <!-- 2. Lo que esta dentro de la silueta pero fuera del difuso -->
  <feComposite in="SourceAlpha" in2="difuso" operator="out" result="borde" />
  <!-- 3. Teñirlo -->
  <feFlood flood-color="#11111b" flood-opacity="0.55" result="tinta" />
  <feComposite in="tinta" in2="borde" operator="in" result="sombra" />
  <!-- 4. Encima del original -->
  <feComposite in="sombra" in2="SourceGraphic" operator="over" />
</filter>

El paso clave es el operator="out": conserva la parte de SourceAlpha donde difuso no tiene alfa, lo que da exactamente la franja interior del borde superior. Si inviertes el signo de dy, la sombra pasa al borde inferior.

Nótese que el resultado ya está recortado a la silueta, así que el over final no necesita más recorte.

Efecto 3: resplandor de color

Un halo del color que elijas, alrededor de la forma, sin teñir el interior.

<filter id="resplandor" x="-60%" y="-60%" width="220%" height="220%"
        color-interpolation-filters="sRGB">
  <feGaussianBlur in="SourceAlpha" stdDeviation="7" result="difuso" />
  <feFlood flood-color="#89b4fa" flood-opacity="1" result="color" />
  <feComposite in="color" in2="difuso" operator="in" result="halo" />
  <!-- Quitar del halo la parte que cae dentro de la silueta -->
  <feComposite in="halo" in2="SourceAlpha" operator="out" result="halo-exterior" />
  <feMerge>
    <feMergeNode in="halo-exterior" />
    <feMergeNode in="halo-exterior" />
    <feMergeNode in="SourceGraphic" />
  </feMerge>
</filter>

Dos detalles. El segundo feComposite con out es lo que hace que el halo no manche el interior de la forma, y es la diferencia entre un resplandor limpio y uno que apaga el color del objeto.

Y el feMergeNode repetido: apilar la misma capa dos veces intensifica el halo, porque la composición over de una capa semitransparente consigo misma aumenta su opacidad efectiva. Es la forma barata de subir la intensidad sin recalcular el desenfoque, y se puede repetir tres o cuatro veces.

Efecto 4: sombra larga

La sombra plana y alargada del estilo de diseño de hace unos años. Se construye apilando desplazamientos progresivos del alfa.

<filter id="larga" x="-10%" y="-10%" width="200%" height="200%"
        color-interpolation-filters="sRGB">
  <feOffset in="SourceAlpha" dx="2"  dy="2"  result="d1" />
  <feOffset in="SourceAlpha" dx="4"  dy="4"  result="d2" />
  <feOffset in="SourceAlpha" dx="6"  dy="6"  result="d3" />
  <feOffset in="SourceAlpha" dx="8"  dy="8"  result="d4" />
  <feOffset in="SourceAlpha" dx="10" dy="10" result="d5" />
  <feMerge result="acumulado">
    <feMergeNode in="d1" /><feMergeNode in="d2" /><feMergeNode in="d3" />
    <feMergeNode in="d4" /><feMergeNode in="d5" />
  </feMerge>
  <feFlood flood-color="#11111b" flood-opacity="0.25" result="tinta" />
  <feComposite in="tinta" in2="acumulado" operator="in" result="sombra" />
  <feComposite in="sombra" in2="SourceGraphic" operator="over" />
</filter>

Con cinco desplazamientos la sombra tiene escalones visibles en los bordes diagonales; con doce y un paso de 1 unidad queda continua. Es un ejemplo claro del compromiso entre calidad y número de primitivas, y del hecho de que un filtro con veinte primitivas es perfectamente legal y perfectamente caro.

Efecto 5: borde suave por dentro

Una viñeta que oscurece los bordes de una imagen sin tocar el centro. Se hace desenfocando el alfa hacia dentro con feMorphology, que se ve en la lección de ruido, o más simplemente con una máscara. Pero hay una variante puramente de filtro que merece conocerse: desenfocar el alfa y usarlo como alfa del original.

<filter id="difuminado" x="-20%" y="-20%" width="140%" height="140%"
        color-interpolation-filters="sRGB">
  <feGaussianBlur in="SourceAlpha" stdDeviation="10" result="alfa-suave" />
  <feComposite in="SourceGraphic" in2="alfa-suave" operator="in" />
</filter>

El resultado es el elemento original con los bordes desvanecidos, sin desenfocar su contenido. Es la diferencia entre difuminar el borde y difuminar la imagen, y es un efecto que la gente intenta con feGaussianBlur sobre SourceGraphic y sale mal.

El coste del desenfoque no es lineal, y hay un truco de escala que lo divide por cuatro

Un desenfoque gaussiano exacto cuesta O(n²) por píxel con un núcleo de tamaño n. Todos los motores usan la descomposición en dos pases (horizontal y vertical), que baja a O(n), y muchos aproximan la gaussiana con tres pases de desenfoque de caja, que es una aproximación con error inferior al 3 por ciento y coste constante por píxel. La especificación de filtros describe explícitamente esa aproximación de tres cajas y da las fórmulas del tamaño de caja en función de stdDeviation, lo que es inusual: la especificación autoriza la aproximación.

Aun así, un stdDeviation grande sobre un área grande es caro, y hay un truco que se usa en producción y que casi nadie escribe: reducir la resolución antes de desenfocar. Un desenfoque de desviación 20 sobre una imagen a escala completa da casi el mismo resultado que un desenfoque de desviación 5 sobre la misma imagen a un cuarto de resolución, ampliada después. Y cuesta la sexta parte.

En SVG no hay una primitiva de reescalado directa, pero hay dos maneras de conseguirlo. La primera es aplicar el filtro a un elemento pequeño y escalar el resultado con un transform. La segunda, si el filtro se aplica a una imagen, es servir una versión reducida para la capa desenfocada. Ambas son el mismo principio: el desenfoque destruye información, así que no tiene sentido calcularlo sobre información que vas a destruir.

Y el corolario para el rendimiento en producción: si un filtro de desenfoque hace que una animación se entrecorte, la primera cosa que hay que probar no es reducir stdDeviation, es reducir el área. Un desenfoque grande sobre un elemento pequeño es barato; uno pequeño sobre un elemento a pantalla completa, no.

⚔️ Reto práctico

Replica exactamente un box-shadow de CSS con tres capas usando filtros de SVG, y comprueba superponiendo los dos elementos que coinciden. Necesitarás la conversión entre radio de CSS y desviación típica, el espacio de color correcto, y un feMerge con el orden bien puesto. Si no coinciden, la diferencia te dirá cuál de los tres ajustes te falta.