radialGradient y spreadMethod
El círculo final, el punto focal y el radio focal, qué hacen pad, reflect y repeat, y cómo se construye un degradado cónico que SVG no tiene.
El gradiente radial tiene seis parámetros geométricos y dos de ellos casi nadie usa, lo que es una pena porque son los que producen los efectos interesantes: el punto focal desplaza el centro de la luz sin mover el círculo, y el radio focal convierte el degradado en un anillo. Y spreadMethod, que también se aplica al lineal, es lo que permite construir patrones de bandas repetidas sin declarar veinte paradas.
- Situar el círculo final y el punto focal de un
radialGradient. - Usar
frpara producir un degradado que arranca a cierta distancia del centro. - Aplicar
spreadMethoden sus tres valores y predecir el resultado. - Construir un degradado cónico dentro de un SVG, sabiendo que el formato no lo tiene.
El círculo final y el punto focal
Un radialGradient tiene dos geometrías: el círculo final, definido por cx, cy y r, que marca dónde está la parada de offset 1; y el punto focal, definido por fx, fy y fr, que marca dónde está la parada de offset 0.
Por omisión el foco coincide con el centro (fx y fy toman el valor de cx y cy) y fr vale cero, lo que produce el degradado radial simétrico de toda la vida. Los valores por defecto del círculo final son cx="50%" cy="50%" r="50%".
<svg viewBox="0 0 320 110" width="640">
<defs>
<radialGradient id="simple">
<stop offset="0" stop-color="#f9e2af" /><stop offset="1" stop-color="#f38ba8" />
</radialGradient>
<radialGradient id="foco" fx="0.3" fy="0.3">
<stop offset="0" stop-color="#f9e2af" /><stop offset="1" stop-color="#f38ba8" />
</radialGradient>
<radialGradient id="anillo" fr="0.35">
<stop offset="0" stop-color="#f9e2af" /><stop offset="1" stop-color="#f38ba8" />
</radialGradient>
</defs>
<circle cx="55" cy="55" r="45" fill="url(#simple)" />
<circle cx="160" cy="55" r="45" fill="url(#foco)" />
<circle cx="265" cy="55" r="45" fill="url(#anillo)" />
</svg>
El primero es el degradado neutro. El segundo tiene el punto de luz arriba a la izquierda, con las bandas de color deformándose alrededor de él: es el aspecto de una esfera iluminada, y es la razón de que fx y fy existan. Mover el foco es mucho más convincente que mover el centro entero, porque el borde exterior se queda quieto.
El tercero usa fr, el radio focal, que es una adición de SVG 2 disponible hoy en los tres motores. Con fr="0.35", todo el interior de ese radio es del color de la primera parada, y el degradado ocurre entre fr y r. Es la forma correcta de hacer un halo alrededor de un objeto sólido, y evita el apaño de meter dos paradas al mismo offset.
La restricción que hay que respetar: el punto focal debe estar dentro del círculo final. Si fx, fy cae fuera, el comportamiento no es el que esperas; SVG 1.1 lo desplazaba al borde y SVG 2 cambió la definición, así que el resultado no es idéntico entre motores. Mantén el foco dentro y no tendrás el problema.
spreadMethod
spreadMethod decide qué pasa fuera del rango del gradiente: más allá del final del vector en el lineal, más allá del círculo final en el radial. Acepta tres valores.
pad es el valor por omisión: el color de la última parada se extiende indefinidamente. Es lo que quieres el noventa por ciento de las veces.
reflect repite el gradiente en espejo, alternando dirección. Produce bandas simétricas sin costura.
repeat repite el gradiente desde el principio en cada ciclo, lo que produce un salto duro en cada repetición salvo que la primera y la última parada sean del mismo color.
<svg viewBox="0 0 320 80" width="640">
<defs>
<linearGradient id="p" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="20" y2="0"
spreadMethod="pad">
<stop offset="0" stop-color="#89b4fa" /><stop offset="1" stop-color="#cba6f7" />
</linearGradient>
<linearGradient id="rf" href="#p" spreadMethod="reflect" />
<linearGradient id="rp" href="#p" spreadMethod="repeat" />
</defs>
<rect x="0" y="5" width="100" height="20" fill="url(#p)" />
<rect x="0" y="30" width="100" height="20" fill="url(#rf)" />
<rect x="0" y="55" width="100" height="20" fill="url(#rp)" />
</svg>
Nótese que los tres heredan las paradas del primero con href, y que el vector es de solo 20 unidades sobre un rectángulo de 100: sin eso, no habría nada fuera del rango y spreadMethod no tendría efecto.
spreadMethod con reflect y un vector corto es la manera más económica de hacer un patrón de rayas suaves: dos paradas y un atributo, frente a un pattern completo. Con repeat y paradas duras, salen rayas nítidas:
<linearGradient id="rayas" gradientUnits="userSpaceOnUse"
x1="0" y1="0" x2="8" y2="8" spreadMethod="repeat">
<stop offset="0" stop-color="#313244" />
<stop offset="0.5" stop-color="#313244" />
<stop offset="0.5" stop-color="#45475a" />
<stop offset="1" stop-color="#45475a" />
</linearGradient>
Rayas diagonales de 8 unidades de paso. Comparado con el pattern equivalente, pesa la mitad y no tiene el problema de las costuras.
El degradado cónico que SVG no tiene
CSS tiene conic-gradient. SVG no tiene nada equivalente, y es una carencia real porque el degradado cónico es exactamente lo que hace falta para un indicador circular de progreso, una ruleta de color o un gráfico de tarta con transición continua.
Hay tres maneras de construirlo y conviene conocer las tres porque sirven en contextos distintos.
Uno: sectores discretos. Divides el círculo en n sectores y le das a cada uno un color interpolado. Con 60 sectores el resultado es indistinguible de un cónico real a tamaños normales.
function conico(cx, cy, r, colores, n = 90) {
const piezas = [];
for (let i = 0; i < n; i++) {
const a0 = (i / n) * 2 * Math.PI - Math.PI / 2;
const a1 = ((i + 1) / n) * 2 * Math.PI - Math.PI / 2;
// Un grado de solape evita las costuras del antialiasing
const a1s = a1 + 0.004;
const p = a => [cx + r * Math.cos(a), cy + r * Math.sin(a)];
const [x0, y0] = p(a0), [x1, y1] = p(a1s);
const d = `M ${cx} ${cy} L ${x0} ${y0} A ${r} ${r} 0 0 1 ${x1} ${y1} Z`;
piezas.push(`<path d="${d}" fill="${colores(i / n)}" />`);
}
return piezas.join('');
}
El + 0.004 de solape es la parte que nadie te cuenta: sin él, entre sector y sector aparece una línea del color del fondo, porque el antialiasing de dos bordes adyacentes no llega a cubrir el píxel entero. Con el solape desaparece.
Dos: una imagen de fondo CSS. Si el SVG está en línea, un foreignObject con un div que tenga background: conic-gradient(...) funciona. Es una solución fea que resuelve el caso, y no sobrevive a servir el SVG como imagen.
Tres: usar el cónico de CSS y el SVG solo como máscara. Un div con conic-gradient de fondo, recortado con mask-image apuntando a la forma del SVG. Es la solución más limpia cuando estás en una página web y no en un fichero SVG autocontenido.
El artefacto que arruina más degradados grandes no tiene que ver con SVG: es la profundidad de color del búfer. En 8 bits por canal solo hay 256 niveles de cada componente, así que un degradado de un azul oscuro a otro azul oscuro ligeramente distinto, extendido sobre 800 píxeles, tiene que repartir quizá 12 niveles entre 800 píxeles. El resultado son bandas de unos 66 píxeles de ancho, perfectamente visibles.
No se arregla añadiendo paradas: el problema no es la interpolación, es la cuantización final. Se arregla de una de estas tres formas.
La primera y más eficaz es añadir ruido. Un feTurbulence de amplitud mínima superpuesto rompe las bandas porque desplaza el redondeo de cada píxel de forma pseudoaleatoria: es tramado, el mismo truco de las imprentas. Un baseFrequency alto y una opacidad del 2 o 3 por ciento bastan, y las primitivas están en el nivel 22.
La segunda es aumentar el contraste del degradado, si el diseño lo permite: cuantos más niveles haya entre los extremos, más finas son las bandas.
La tercera es reducir el área: el mismo degradado en un elemento pequeño no muestra bandas porque los saltos caen más juntos.
Lo que casi nunca funciona es cambiar el espacio de interpolación. Un degradado interpolado en OKLab se ve mejor en su recorrido de matices, pero sigue cuantizándose a 8 bits al final y sigue produciendo bandas si el contraste es bajo.
Construye un indicador circular de progreso con degradado cónico usando la función de sectores, y compáralo con la alternativa de un circle con stroke-dasharray y un gradiente lineal. Comprueba dos cosas: cuál se ve mejor en el punto donde el arco se cierra sobre sí mismo, y cuántos nodos del DOM tiene cada uno. Después decide a partir de qué tamaño de componente merece la pena la versión de 90 sectores.