La región del filtro y el espacio de color
Por qué las sombras salen cortadas, cómo se calcula la región por omisión, qué hacen filterUnits y primitiveUnits, y la propiedad que cambia el aspecto de todos tus filtros.
Dos cosas arruinan más filtros que cualquier error de primitivas: una región demasiado pequeña que corta el efecto con un rectángulo invisible, y un espacio de color que nadie eligió y que hace que todo salga distinto de lo esperado. Las dos se arreglan con un atributo cada una, y las dos hay que ponerlas prácticamente siempre.
- Calcular la región por omisión de un filtro y saber cuándo se queda corta.
- Ampliar la región con
x,y,widthyheighten los dos sistemas de unidades. - Explicar qué hace
primitiveUnitsy por qué casi nunca conviene cambiarlo. - Fijar
color-interpolation-filtersy justificar por qué el valor inicial no es el que quieres.
La región por omisión
Un filter tiene una región rectangular fuera de la cual el resultado se descarta. Sus valores por omisión, con filterUnits="objectBoundingBox":
x="-10%" y="-10%" width="120%" height="120%"
Es decir, la caja envolvente del elemento ampliada un diez por ciento por cada lado. Ese margen basta para un desenfoque pequeño y se queda corto para casi todo lo demás.
El caso más común: una sombra desplazada.
<!-- La sombra se corta por abajo y por la derecha -->
<filter id="corta">
<feDropShadow dx="8" dy="8" stdDeviation="6" flood-opacity="0.5" />
</filter>
<!-- Region suficiente -->
<filter id="entera" x="-30%" y="-30%" width="180%" height="180%">
<feDropShadow dx="8" dy="8" stdDeviation="6" flood-opacity="0.5" />
</filter>
Sobre un elemento de 100 unidades, el diez por ciento son 10 unidades de margen. La sombra se desplaza 8 y se desenfoca con desviación 6, y un desenfoque gaussiano se extiende de forma apreciable hasta unas tres desviaciones típicas, o sea 18 unidades. Total: 26 unidades de alcance frente a 10 de margen. La sombra sale con un borde recto.
La regla para dimensionar la región, aproximada y suficiente:
margen = |desplazamiento| + 3 * stdDeviation
Y como el margen se expresa en porcentaje de la caja, hay que dividir por el tamaño del elemento. En la práctica, la mayoría de la gente escribe x="-50%" y="-50%" width="200%" height="200%" y se olvida. No es elegante y funciona: el coste de una región grande es memoria y tiempo, no corrección, y en un elemento pequeño es despreciable.
En un elemento grande sí importa. Duplicar cada dimensión cuadruplica el área de los búferes, y con doce primitivas eso es mucha memoria. Ahí conviene calcular.
filterUnits en espacio de usuario
filterUnits="userSpaceOnUse" hace que x, y, width y height sean coordenadas del lienzo. Es lo que quieres cuando la región debe ser fija e independiente del elemento, y cuando el elemento tiene una caja envolvente degenerada.
<filter id="global" filterUnits="userSpaceOnUse"
x="0" y="0" width="400" height="300">
<feDropShadow dx="4" dy="4" stdDeviation="4" />
</filter>
Y aquí conecta con la trampa del nivel 20: con objectBoundingBox y una caja envolvente de área cero, el filtro no se aplica en absoluto y el elemento no se pinta. Una línea horizontal con un filtro desaparece. La causa es que la región tiene área cero, y el arreglo es filterUnits="userSpaceOnUse" con dimensiones explícitas.
primitiveUnits
primitiveUnits controla cómo se interpretan las longitudes dentro de las primitivas: stdDeviation, dx, dy, radius, scale, x e y de cada primitiva. Su valor por omisión es userSpaceOnUse, o sea unidades del lienzo.
El otro valor, objectBoundingBox, hace que esas longitudes sean fracciones de la caja. Suena útil (un desenfoque que escala con el elemento) y en la práctica casi nunca se usa, por dos razones.
La primera: los números se vuelven ilegibles. Un stdDeviation="0.02" no dice nada, y hay que dividir mentalmente por el tamaño para saber si es mucho o poco.
La segunda: la deformación. Igual que en los gradientes, la caja no cuadrada escala cada eje de forma distinta, así que un desenfoque isotrópico se convierte en direccional. Un stdDeviation="0.02" en un elemento de 200 por 50 desenfoca 4 unidades en horizontal y 1 en vertical.
Recomendación: deja primitiveUnits en su valor por omisión. Si necesitas un filtro que escale, genera el valor de stdDeviation desde el código, o usa un filtro distinto por tamaño.
color-interpolation-filters
Esta es la propiedad que cambia el aspecto de todos tus filtros y que casi nadie conoce.
Los filtros de SVG operan sobre valores de color, y hay que decidir en qué espacio. La propiedad color-interpolation-filters acepta sRGB y linearRGB, y su valor inicial es linearRGB.
linearRGB es el espacio físicamente correcto: los valores son proporcionales a la energía luminosa, así que sumar dos luces da el resultado que daría en la realidad. sRGB es el espacio en el que están codificados los colores de tu diseño, con la curva de gamma que corrige la percepción.
La consecuencia de operar en linearRGB: los tonos medios se aclaran. Un desenfoque gaussiano en linearRGB produce un halo notablemente más claro que el mismo desenfoque en sRGB. Una sombra sale más suave y menos densa. Un feColorMatrix de duotono da colores que no son los que pusiste.
Y el detalle que remata: CSS hace lo contrario. Las funciones de filter de CSS (blur(), drop-shadow()) están especificadas para operar en sRGB. De modo que el mismo desenfoque escrito como filter: blur(4px) y como <feGaussianBlur stdDeviation="4"> no se ve igual, y esa es la explicación del misterio de «mi sombra de SVG no coincide con la de CSS».
<filter id="f" color-interpolation-filters="sRGB">
<feGaussianBlur stdDeviation="4" />
</filter>
Un atributo. Ponlo en todos tus filtros salvo que sepas exactamente por qué quieres el otro. Como es una propiedad heredada, también puedes ponerlo una vez en el svg raíz y que baje a todos los filtros del documento.
La elección de linearRGB como valor inicial procede de la ambición original de SVG: ser un formato de gráficos correcto desde el punto de vista de la reproducción del color, para artes gráficas e impresión. En ese contexto, componer en espacio lineal es la respuesta correcta y la que da resultados predecibles al cambiar de perfil de color.
En 2001 esa decisión tenía sentido. Hoy tiene tres problemas.
Es más lento. Cada primitiva tiene que convertir de sRGB a lineal a la entrada y de vuelta a la salida, o mantener búferes de mayor precisión. En un filtro de diez primitivas eso es trabajo real, y algunos motores han tenido rutas rápidas solo para sRGB.
No coincide con nada de lo que hay alrededor. El resto de la web compone en sRGB: CSS, canvas, la composición de capas del navegador. Un filtro de SVG es la única pieza que trabaja en otro espacio, así que es la única que no encaja.
Nadie lo espera. El diseñador que eligió el color de la sombra lo eligió mirando una pantalla, y el resultado en lineal no es ese color.
La ironía es que la decisión correcta hoy no sería ni sRGB ni linearRGB, sino un espacio perceptualmente uniforme, que es a lo que ha llegado CSS con la interpolación en OKLab. Los filtros de SVG no tienen esa opción, y la especificación de efectos de filtro lleva años sin movimiento en ese punto.
Práctica: color-interpolation-filters="sRGB" en todos los filtros, sin excepciones, salvo que estés replicando un efecto concreto que se diseñó en lineal. Y si un filtro tuyo se ve raro y no sabes por qué, esta es la primera cosa que hay que probar.
Pinta el mismo elemento cuatro veces: con filter: blur(6px) de CSS, con un feGaussianBlur stdDeviation="6" sin tocar el espacio de color, con el mismo pero con color-interpolation-filters="sRGB", y con la región por omisión frente a una ampliada. Compara los cuatro y anota qué par coincide. Esa comparación es el mejor argumento para la regla del artículo.