wandres.dev
FILTROS SVG · El pipeline de primitivas

El pipeline de primitivas: in y result

Un filtro es un grafo de operaciones sobre imágenes. Las entradas implícitas, el encadenamiento por nombre, las ramas paralelas, y las dos primitivas que vuelven a juntarlas.

⏱ 19 min

Los filtros de SVG son la parte más potente del formato y la peor documentada: una máquina de composición de imágenes completa, con entradas nombradas, resultados intermedios y ramas paralelas, escondida detrás de una sintaxis XML que no deja ver el grafo. En cuanto entiendes que estás escribiendo un grafo de operaciones y no una lista de efectos, todo lo demás encaja.

🎯 Al terminar esta lección sabrás
  • Describir un filtro como un grafo dirigido de primitivas con entradas y salidas.
  • Usar in y result para encadenar y para bifurcar.
  • Enumerar las entradas predefinidas y decir cuáles están realmente implementadas.
  • Combinar dos ramas con feComposite y con feBlend, sabiendo qué hace cada uno.

El modelo: imágenes que entran y salen

Un filter contiene una secuencia de primitivas, cada una de las cuales toma una o dos imágenes de entrada, hace una operación por píxel o por vecindad, y produce una imagen de salida. La última primitiva de la lista es la que se pinta.

Cada primitiva tiene:

  • in: el nombre de la imagen de entrada. Si se omite, la regla es doble: en la primera primitiva vale SourceGraphic; en cualquier otra, vale el resultado de la primitiva anterior.
  • in2: la segunda entrada, en las primitivas que necesitan dos.
  • result: el nombre con el que se guarda la salida, para poder referenciarla más adelante. Si se omite, el resultado solo está disponible como entrada implícita de la siguiente.

Con esas tres reglas se construye cualquier filtro.

<filter id="cadena">
  <feGaussianBlur stdDeviation="4" />           <!-- in implicito = SourceGraphic -->
  <feOffset dx="4" dy="4" />                     <!-- in implicito = el desenfoque -->
  <feColorMatrix type="saturate" values="0" />   <!-- in implicito = el desplazado -->
</filter>

Tres primitivas, ninguna con in ni result, y funciona: es una cadena lineal. La mayoría de los filtros útiles, sin embargo, no son lineales.

Las entradas predefinidas

Nombre Qué es Estado
SourceGraphic El elemento tal y como se habría pintado Implementado
SourceAlpha Solo el canal alfa del elemento, en negro Implementado
BackgroundImage Lo que hay pintado detrás Nunca implementado
BackgroundAlpha El alfa de lo de detrás Nunca implementado
FillPaint El relleno del elemento, extendido al infinito Soporte muy desigual
StrokePaint El trazo del elemento, extendido al infinito Soporte muy desigual

Merece la pena detenerse en BackgroundImage. La especificación lo definió para permitir efectos que leen el fondo, del tipo cristal esmerilado. Ningún motor lo implementó nunca, porque exige que el compositor tenga disponible el resultado de todo lo pintado hasta ese momento en el momento de aplicar el filtro, lo que rompe la posibilidad de paralelizar el pintado. La función existe hoy en CSS con backdrop-filter, que resuelve el mismo problema con otro modelo. Si encuentras un tutorial que usa BackgroundImage, es de 2005 y no funciona.

SourceAlpha es la que se usa constantemente y conviene tenerla clara: es la silueta del elemento, con el color a negro y el alfa original. Es la entrada correcta para una sombra, porque una sombra no debe tener el color del objeto.

Bifurcar y volver a juntar

El grafo se ramifica cuando dos primitivas usan la misma entrada, y se vuelve a juntar con una primitiva de dos entradas.

flowchart TB
SG[SourceGraphic] --> A[feGaussianBlur stdDeviation 6]
SG --> M[feMerge nodo 2]
A -->|result desenfoque| B[feFlood flood-color azul]
B -->|result color| C[feComposite operator in]
A --> C
C -->|result halo| M
M --> OUT[Salida pintada]
style SG fill:#89b4fa,color:#11111b
style A fill:#cba6f7,color:#11111b
style B fill:#cba6f7,color:#11111b
style C fill:#f9e2af,color:#11111b
style M fill:#94e2d5,color:#11111b
style OUT fill:#a6e3a1,color:#11111b

Ese grafo es el de un resplandor, y en XML se escribe así:

<filter id="halo" x="-50%" y="-50%" width="200%" height="200%"
        color-interpolation-filters="sRGB">
  <feGaussianBlur in="SourceGraphic" stdDeviation="6" result="desenfoque" />
  <feFlood flood-color="#89b4fa" flood-opacity="0.9" result="color" />
  <feComposite in="color" in2="desenfoque" operator="in" result="halo" />
  <feMerge>
    <feMergeNode in="halo" />
    <feMergeNode in="SourceGraphic" />
  </feMerge>
</filter>

Léelo de arriba abajo con el grafo delante. Se desenfoca el original y se guarda como desenfoque. Se genera un rectángulo de color plano que llena toda la región del filtro. Se recorta ese color con la forma del desenfoque usando operator="in", lo que produce un halo del color elegido con la forma difusa del original. Y se apilan halo y original con feMerge, que pinta sus nodos en orden, el primero abajo.

La clave del patrón está en feComposite operator="in": es la manera de teñir una forma. in conserva la parte de la primera entrada que solapa con el alfa de la segunda, así que un color plano recortado por un alfa da ese alfa del color.

Las dos primitivas de unión

feComposite implementa los operadores de Porter y Duff, que son los del álgebra de composición de imágenes:

operator Resultado
over La primera encima de la segunda. Es el valor por omisión
in La primera, recortada al alfa de la segunda
out La primera, donde la segunda no tiene alfa
atop La primera sobre la segunda, recortada al alfa de la segunda
xor Donde solo una de las dos tiene alfa
arithmetic Una combinación lineal con cuatro coeficientes

arithmetic calcula, por canal y con los valores premultiplicados:

resultado = k1 * i1 * i2 + k2 * i1 + k3 * i2 + k4

Con k2="1" k3="1" es una suma; con k2="1" k3="-1" es una resta; con k1="1" es una multiplicación. Es la primitiva que resuelve los casos que ningún operador con nombre cubre, y aparece en filtros de iluminación y de contraste.

feBlend implementa los modos de fusión, los mismos de CSS: normal, multiply, screen, overlay, darken, lighten, color-dodge, color-burn, hard-light, soft-light, difference, exclusion, hue, saturation, color y luminosity.

La diferencia entre los dos: feComposite opera sobre el alfa y decide qué píxeles sobreviven; feBlend opera sobre el color y decide cómo se mezclan. Cuando quieres recortar una imagen con la silueta de otra, feComposite. Cuando quieres multiplicar una textura sobre una forma, feBlend.

El orden de las primitivas es el orden de ejecución, y una primitiva sin usar sigue costando

El motor evalúa las primitivas en el orden en que aparecen, guardando cada result en un búfer del tamaño de la región del filtro. Tres consecuencias que no se anuncian.

Una primitiva cuyo result nadie usa se ejecuta igualmente. No hay eliminación de código muerto. Un filtro con un feTurbulence de prueba que dejaste ahí y que ya no alimenta nada consume su tiempo de cálculo en cada fotograma. Y feTurbulence es la primitiva más cara de todas: generar ruido de Perlin con varias octavas para una región de 600 por 400 son cientos de miles de evaluaciones. Revisar un filtro complejo buscando ramas huérfanas es una optimización real, no teórica.

Una primitiva solo puede referenciar resultados anteriores. El grafo tiene que ser acíclico y estar en orden topológico en el XML. Si un in apunta a un result que se define más abajo, la referencia no se resuelve y el motor sustituye la entrada por SourceGraphic. Sin ningún aviso. El síntoma es un filtro que produce algo parecido a lo que querías pero con una capa que no corresponde, y la causa es una primitiva movida de sitio. Cuando reordenes un filtro, revisa las referencias.

Cada result es un búfer completo. Un filtro con doce primitivas mantiene hasta doce imágenes en memoria del tamaño de la región. Sobre un elemento a pantalla completa en una pantalla de alta densidad, eso son decenas de megabytes. Es la razón de que un filtro que funciona bien en el escritorio ahogue un móvil de gama media, y de que reducir la región del filtro sea a menudo la optimización que más rinde.

Depurar un filtro

Tres técnicas que ahorran horas.

Aislar la rama. Comenta las primitivas posteriores y deja que la que investigas sea la última. Como la última es la que se pinta, ves su salida directamente.

Pintar un resultado intermedio. Añade al final un feComposite operator="in" in="X" in2="X" que no cambie nada, con in apuntando al resultado que quieres ver. Más simple todavía: mueve un feMerge con un solo nodo al final.

Nombrar todo. Aunque la cadena sea lineal, poner result en cada primitiva y in explícito en cada una convierte un filtro ilegible en un grafo que se lee. Cuesta unos bytes y ahorra la mitad de los errores.

⚔️ Reto práctico

Escribe el filtro del resplandor de este artículo y después modifícalo para que el halo tenga un color distinto del elemento y aparezca solo por fuera de la silueta, sin teñir el interior. Necesitarás una primitiva más y un operador de composición distinto. Dibuja el grafo antes de escribir el XML.