Anatomía del atributo d
El atributo d es un lenguaje con gramática propia: tokens, comandos implícitos, mayúsculas y minúsculas, y las reglas de separación que hacen legal escribir M10-20L.5.5.
El atributo d de un path no es una lista de coordenadas: es un programa. Tiene un alfabeto de diez comandos, un estado (el punto actual y el punto de control reflejado), reglas de repetición implícita y una gramática de separadores tan permisiva que permite escribir cosas que parecen ruido y son válidas. Entender que es un lenguaje, y no un formato de datos, cambia por completo la manera de leerlo y de generarlo.
- Describir el modelo de ejecución de una
d: punto actual, punto inicial de subruta y punto de control reflejado. - Distinguir comando absoluto de relativo y saber cuándo cada uno produce una cadena más corta o más robusta.
- Aplicar la repetición implícita de comandos y la regla especial de
M. - Leer una
dminificada donde los separadores han desaparecido.
Un intérprete con tres registros
Cuando el motor procesa una d, mantiene tres piezas de estado, y todos los comportamientos raros salen de ahí.
El punto actual es dónde está el lápiz. Empieza indefinido: por eso una d que no comienza con M es un error y no se dibuja nada. Cada comando de dibujo lo actualiza.
El punto inicial de la subruta es adónde vuelve Z. Se fija con cada M y no cambia hasta el siguiente M. Una d puede tener muchas subrutas, y cada Z cierra la suya.
El punto de control reflejado es el estado que sirve a S y a T. Guarda el último punto de control de la curva anterior para poder espejarlo. Si el comando anterior no era del tipo compatible, se considera que coincide con el punto actual.
Con esos tres registros, el alfabeto completo es de diez comandos:
| Comando | Nombre | Parámetros por repetición |
|---|---|---|
M |
moveto | 2 |
L |
lineto | 2 |
H |
horizontal lineto | 1 |
V |
vertical lineto | 1 |
C |
curveto cúbico | 6 |
S |
shorthand cúbico | 4 |
Q |
curveto cuadrático | 4 |
T |
shorthand cuadrático | 2 |
A |
arco elíptico | 7 |
Z |
closepath | 0 |
Y nada más. No hay condicionales, no hay bucles, no hay variables. Todo lo que se dibuja en la web con vectores sale de estas diez letras.
Mayúscula y minúscula
Cada comando tiene dos formas. La mayúscula interpreta sus coordenadas en el sistema de coordenadas del usuario, en absoluto. La minúscula las interpreta como un desplazamiento respecto al punto actual.
<!-- Absoluto: cada punto se lee tal cual -->
<path d="M 20 20 L 80 20 L 80 60 L 20 60 Z" />
<!-- Relativo: cada numero es un incremento -->
<path d="m 20 20 l 60 0 l 0 40 l -60 0 z" />
Las dos dibujan el mismo rectángulo. Las diferencias prácticas son tres y cada una manda en un contexto distinto.
El peso. En una forma orgánica con cientos de puntos cercanos, las coordenadas relativas son números pequeños con pocos dígitos: l3.2 1.1 frente a L 415.4 288.7. Es una de las razones por las que los optimizadores convierten a relativo, y por las que un SVG de icono exportado parece ilegible. La ganancia real puede rondar el 20 o el 30 por ciento del tamaño de la d antes de comprimir; después de Brotli la diferencia se reduce bastante, porque los números repetidos comprimen bien.
La robustez ante la edición. Con coordenadas absolutas puedes cambiar un punto y el resto no se mueve. Con relativas, cambiar un valor desplaza todo lo que viene después. Para una ruta generada por código eso es irrelevante; para una que un humano va a tocar es una tortura.
La composición. Trasladar una subruta relativa entera es cambiar un solo m inicial. Trasladar una absoluta es sumar el desplazamiento a todos los pares. Cuando construyes un sprite pegando trozos, relativo es lo que quieres.
Z y z son idénticos: no llevan parámetros, así que no hay nada que interpretar en absoluto ni en relativo.
La trampa más fina de todo el mini-lenguaje. Z devuelve el punto actual al punto inicial de la subruta. De modo que un m que venga inmediatamente después de un z no se mide desde donde terminaste de dibujar, sino desde donde empezaste la subruta. Esto se ve constantemente en iconos con varias piezas:
<path d="M 10 10 h 20 v 20 h -20 z m 30 0 h 20 v 20 h -20 z" />Ese m 30 0 no parte del punto (10, 30) donde el último h -20 dejó el lápiz, sino de (10, 10), el inicio de la subruta cerrada por z. El segundo cuadrado empieza en (40, 10). Si asumes lo contrario, la segunda pieza aparece desplazada 20 unidades hacia abajo y pasas media hora buscando un error de aritmética que no existe. Y cuidado con el caso simétrico: sin z entre medias, el m sí parte del último punto dibujado.
Repetición implícita
Un comando puede ir seguido de varios juegos de parámetros. El motor repite el mismo comando por cada juego. Es la regla que hace que las d reales sean mucho más cortas de lo que serían con un comando por segmento:
<!-- Explicito -->
<path d="M 10 10 L 40 10 L 40 40 L 10 40 Z" />
<!-- Con repeticion implicita -->
<path d="M 10 10 L 40 10 40 40 10 40 Z" />
La regla vale para todos los comandos con parámetros, incluido A con sus siete valores por repetición. Y tiene una excepción, la de M: los juegos de coordenadas que siguen al primero en un M no se interpretan como más moveto sino como lineto. Un M 10 10 20 20 30 10 equivale a M 10 10 L 20 20 L 30 10. Con m minúscula, las repeticiones son l relativas.
La razón de esa excepción es puramente pragmática: dos moveto seguidos no tienen sentido geométrico, porque el primero no dibuja nada y el segundo lo sobrescribe. Aprovechar la sintaxis para el caso útil ahorra una letra en la construcción más frecuente del formato.
La gramática de los separadores
Aquí está la parte que hace ilegibles las d minificadas. Las reglas exactas:
- El espacio en blanco entre tokens es opcional siempre que el token siguiente se pueda distinguir sin él.
- Una letra de comando siempre delimita: no hace falta espacio antes ni después.
- Un signo
-delimita un número, porque no puede aparecer en mitad de uno salvo trase. - Un
.empieza un número nuevo si el número anterior ya tenía punto decimal. - Las comas son separadores como el espacio, y se pueden mezclar.
De ahí salen construcciones perfectamente legales que parecen erratas:
<path d="M10-20L.5.5.7.9z" />
Que se lee: M 10 -20, luego L 0.5 0.5, luego (repetición implícita) L 0.7 0.9, luego z. El -20 no necesita espacio porque el guion delimita. El .5.5 son dos números porque el segundo punto abre uno nuevo. Y el .7.9 es la repetición implícita del L.
Los números pueden llevar notación exponencial (1e3, 2.5e-4), y ese es el punto donde los parsers escritos a mano suelen fallar. Si vas a analizar d por tu cuenta, no lo hagas con una expresión regular ingenua: usa un tokenizador con estado, o mejor, deja que el navegador lo haga por ti.
// Dejar que el motor parsee y devuelva la geometria muestreada
const p = document.querySelector('path');
const total = p.getTotalLength();
const muestras = Array.from({ length: 101 }, (_, i) => {
const { x, y } = p.getPointAtLength((i / 100) * total);
return [x, y];
});
Ese muestreo no te devuelve los comandos, pero sí la curva, y para la mayoría de los usos reales (medir, colocar objetos a lo largo de una ruta, comparar dos formas) es más útil que la cadena.
SVG 2 define d como una propiedad CSS que acepta path("M 0 0 L 10 10"), lo que en teoría permite animarla e interpolarla desde una hoja de estilos. La implementación no está en los tres motores, así que no construyas nada que dependa de animar d desde CSS. El camino portable sigue siendo escribir el atributo desde JavaScript en cada fotograma, o usar una biblioteca de morphing que haga la interpolación en JavaScript y escriba el atributo.
Escribe un tokenizador de d en unas veinte líneas que devuelva una lista de objetos con comando y parámetros, expandiendo las repeticiones implícitas y aplicando la regla especial de M. Pruébalo contra M10-20L.5.5.7.9z y contra la d de un icono real de tu proyecto, y compara el número de comandos que cuenta tu tokenizador con el número de letras que ves en la cadena.