feColorMatrix y la manipulación del color
La matriz de 4 por 5 explicada término a término, los modos abreviados, feComponentTransfer, y tres efectos completos: duotono, umbral de alfa y el filtro gooey.
feColorMatrix es una multiplicación de matrices por píxel y con eso hace todo lo que se puede hacer con el color de forma lineal: desaturar, rotar el matiz, invertir, teñir, extraer un canal, y el truco menos evidente y más útil de todos, que es amplificar el canal alfa hasta convertir un desenfoque en una forma sólida. Ese último es el que da el efecto gooey, y merece entenderse en lugar de copiarse.
- Leer y escribir una matriz de color de 4 por 5 sabiendo qué hace cada término.
- Usar los tres modos abreviados y decir a qué matriz equivalen.
- Construir un duotono con
feComponentTransfer. - Explicar por qué el filtro gooey funciona y ajustar sus dos constantes.
La matriz
feColorMatrix type="matrix" toma 20 valores que forman una matriz de 4 filas por 5 columnas. Cada fila calcula un canal de salida:
R' = a00*R + a01*G + a02*B + a03*A + a04
G' = a10*R + a11*G + a12*B + a13*A + a14
B' = a20*R + a21*G + a22*B + a23*A + a24
A' = a30*R + a31*G + a32*B + a33*A + a34
Los valores de entrada y salida están en el rango 0 a 1. La quinta columna es un término independiente, un desplazamiento constante.
La identidad, que no cambia nada:
<feColorMatrix type="matrix" values="1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 1 0" />
Con esa plantilla en la cabeza, las manipulaciones se leen solas.
Escala de grises por luminancia. Cada canal de salida es la misma combinación ponderada de los tres de entrada:
<feColorMatrix type="matrix" values="0.2126 0.7152 0.0722 0 0
0.2126 0.7152 0.0722 0 0
0.2126 0.7152 0.0722 0 0
0 0 0 1 0" />
Esos coeficientes son los de la luminancia relativa de la recomendación BT.709, los mismos que usan las máscaras en modo luminancia y los mismos del cálculo de contraste de las pautas de accesibilidad. No son arbitrarios: reflejan la sensibilidad del ojo, que es máxima al verde y mínima al azul.
Invertir. Multiplicar por menos uno y desplazar uno:
<feColorMatrix type="matrix" values="-1 0 0 0 1
0 -1 0 0 1
0 0 -1 0 1
0 0 0 1 0" />
Extraer el canal rojo como escala de grises. Copiar R a los tres canales:
<feColorMatrix type="matrix" values="1 0 0 0 0
1 0 0 0 0
1 0 0 0 0
0 0 0 1 0" />
Es lo que se usa para convertir un mapa de altura de un canal en algo visible.
Los tres modos abreviados
type="saturate" toma un valor de 0 a 1 (y admite mayores que 1 para sobresaturar). Es una interpolación entre la matriz de luminancia y la identidad.
<feColorMatrix type="saturate" values="0" /> <!-- gris total -->
<feColorMatrix type="saturate" values="1.6" /> <!-- sobresaturado -->
type="hueRotate" toma grados y rota el matiz. Por debajo es una matriz construida con senos y cosenos del ángulo, y es una aproximación lineal: no es una rotación en un espacio de matiz perceptual, así que a ángulos grandes la luminosidad se desvía.
type="luminanceToAlpha" descarta el color y pone la luminancia en el canal alfa. Es la primitiva que convierte una imagen en una máscara.
<!-- Una imagen convertida en mascara de transparencia -->
<feColorMatrix type="luminanceToAlpha" />
feComponentTransfer
Donde feColorMatrix hace combinaciones lineales entre canales, feComponentTransfer aplica una función arbitraria a cada canal por separado. Cuatro hijos posibles: feFuncR, feFuncG, feFuncB, feFuncA, cada uno con un type:
type |
Qué hace | Atributos |
|---|---|---|
identity |
Nada | ninguno |
linear |
slope * c + intercept |
slope, intercept |
gamma |
amplitude * c^exponent + offset |
amplitude, exponent, offset |
table |
Interpolación lineal en una tabla | tableValues |
discrete |
Escalones a partir de una tabla | tableValues |
table es el más útil: reparte los valores de la tabla uniformemente entre 0 y 1 e interpola. Con dos valores define un mapeo lineal entre dos extremos; con cinco, una curva.
discrete produce escalones sin interpolar, que es exactamente la posterización.
Efecto: duotono
Un duotono es una imagen en escala de grises remapeada a dos colores. Dos primitivas:
<filter id="duotono" color-interpolation-filters="sRGB">
<!-- 1. A luminancia -->
<feColorMatrix type="matrix" values="0.2126 0.7152 0.0722 0 0
0.2126 0.7152 0.0722 0 0
0.2126 0.7152 0.0722 0 0
0 0 0 1 0" />
<!-- 2. Remapear cada canal entre el color oscuro y el claro -->
<feComponentTransfer>
<feFuncR type="table" tableValues="0.11 0.65" />
<feFuncG type="table" tableValues="0.09 0.86" />
<feFuncB type="table" tableValues="0.18 0.98" />
</feComponentTransfer>
</filter>
Los dos valores de cada tabla son los componentes del color de sombra y del color de luz, en el rango 0 a 1. En el ejemplo, sombras en un azul muy oscuro y luces en un cian claro. Cambiar la paleta es cambiar seis números.
Con tres valores por tabla se controla también el tono medio, lo que permite duotonos con un tercer color en la zona intermedia.
El color-interpolation-filters="sRGB" es obligatorio aquí: en linearRGB los colores que salen no son los que has escrito, y ajustar la paleta se convierte en adivinanza.
Efecto: el filtro gooey
Este es el que hay que entender de verdad. La idea: si desenfocas varias formas, sus bordes se solapan en una zona de alfa intermedio; si después amplificas brutalmente el alfa, esa zona intermedia salta a opaco y las formas parecen fundirse como gotas de mercurio.
<filter id="gooey" color-interpolation-filters="sRGB"
x="-30%" y="-30%" width="160%" height="160%">
<feGaussianBlur in="SourceGraphic" stdDeviation="10" result="difuso" />
<feColorMatrix in="difuso" type="matrix"
values="1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 19 -9" result="goo" />
<feComposite in="SourceGraphic" in2="goo" operator="atop" />
</filter>
La fila del alfa es 0 0 0 19 -9, es decir:
A' = 19 * A - 9
Una función lineal con pendiente muy alta. Con A = 0.47 da A' = -0.07, que se recorta a 0. Con A = 0.53 da A' = 1.07, que se recorta a 1. Es decir, todo lo que esté por debajo de 0,474 se vuelve transparente y todo lo que esté por encima de 0,526 se vuelve opaco: un umbral duro con una transición de 0,05 de ancho, que es lo que da el borde suave pero definido.
Las dos constantes se ajustan así: la pendiente controla la dureza del borde (más alta, más duro) y el término independiente controla dónde está el umbral. El punto medio es -intercept / slope: con 19 y -9 sale 0,474. Si quieres que las formas se fundan antes, baja el umbral a 0,3 usando values="... 19 -5.7".
El feComposite operator="atop" final vuelve a poner el gráfico original encima del resultado gooey, recortado a él. Sin ese paso las formas quedarían de un color plano y sin sus detalles interiores.
Las primitivas de filtro trabajan por defecto con color premultiplicado por el alfa, salvo feColorMatrix y feComponentTransfer, que la especificación obliga a operar sobre valores no premultiplicados. El motor tiene que desmultiplicar antes y volver a multiplicar después.
Esa conversión es exacta salvo en un caso: cuando el alfa es muy pequeño. Desmultiplicar significa dividir por el alfa, y dividir por 0,01 amplifica cualquier error de redondeo por cien. En un búfer de 8 bits, los píxeles del borde exterior del desenfoque tienen valores de color que ya no son fiables.
Cuando después amplificas el alfa por 19, esos píxeles con color basura pasan de invisibles a completamente opacos. El resultado es una orla de píxeles de color impredecible alrededor de las formas fundidas, que se ve como un halo sucio o marrón. Es exactamente el artefacto que hace que un filtro gooey copiado de internet se vea peor de lo que se ve en la demostración original.
Tres cosas lo mitigan. Poner color-interpolation-filters="sRGB" evita las dos conversiones de espacio adicionales, que son otra fuente de error. Bajar la pendiente de 19 a 12 o 10 reduce cuánto se amplifica el ruido, a cambio de un borde algo más blando. Y usar formas de color plano y sin transparencia hace que el color desmultiplicado sea constante y el error no se note.
Lo que no lo arregla es aumentar stdDeviation, que empeora las cosas al extender más la zona de alfa bajo. Si el halo sucio persiste, la señal es que estás trabajando con contenido de color variable, y la solución es aplicar el gooey a una capa de siluetas planas y poner el contenido encima con una máscara.
Ajusta el filtro gooey para tres tamaños de forma distintos y anota qué relación hay entre stdDeviation y el tamaño de la forma para que la fusión ocurra a la distancia correcta. Después calcula, para una pendiente dada, cuál debe ser el término independiente para que el umbral caiga en 0,35, y comprueba visualmente que las formas se funden desde más lejos.