Los comandos rectos: M, L, H, V y Z
Subrutas, el ahorro real de H y V, por qué Z no es lo mismo que volver al primer punto, y cómo la regla de relleno decide qué queda dentro cuando las subrutas se solapan.
Los cinco comandos rectos parecen los aburridos y son los que más geometría real construyen: toda la cartografía, todos los diagramas técnicos y la mitad de los iconos son segmentos. También son donde aparece el concepto de subruta, que es lo que permite que un solo path dibuje un donut, una letra con contrachapado o un país con islas.
- Construir un
pathcon varias subrutas y predecir dónde vuelve cadaZ. - Cuantificar el ahorro de
HyVfrente aLen una geometría ortogonal. - Explicar la diferencia visual entre cerrar con
Zy volver al punto inicial conL. - Aplicar
fill-rulepara producir un agujero y saber qué papel juega el sentido de giro.
Subrutas: varios trazos en un elemento
Cada M abre una subruta. Un path puede tener tantas como quieras, y todas comparten el mismo relleno, el mismo trazo y el mismo nodo del DOM.
<svg viewBox="0 0 120 60" width="240">
<path d="M 10 10 H 50 V 50 H 10 Z
M 70 10 H 110 V 50 H 70 Z"
fill="#89b4fa" />
</svg>
Dos cuadrados, un elemento. Esto es lo que convierte a path en la herramienta de las rejillas: las cien líneas de una rejilla de gráfico son cien subrutas de dos comandos en un único nodo, en lugar de cien elementos line. En un gráfico con ejes densos, esa diferencia se nota en el tiempo de recálculo de estilos, porque el motor tiene que emparejar los selectores CSS contra cada elemento y aquí hay uno solo.
// Rejilla horizontal completa en una sola d
function rejillaH(x0, x1, ys) {
return ys.map(y => `M${x0} ${y}H${x1}`).join('');
}
rejillaH(40, 380, [20, 60, 100, 140, 180]);
El límite de esta técnica es que todas las subrutas comparten estilo y evento. Si necesitas colorear una línea de rejilla distinta o capturar el hover de una sola, tienen que ser elementos separados.
H y V: el ahorro que se acumula
H toma una sola coordenada x y traza un segmento horizontal hasta ella; V hace lo propio con y. Son azúcar sintáctico sobre L, y su valor es puramente de tamaño y de intención.
En una geometría ortogonal (rectángulos, escaleras, diagramas de bloques, el trazado de un mapa de calles en cuadrícula) todos los segmentos son horizontales o verticales, así que H y V eliminan la mitad de los números. Un rectángulo pasa de M 10 10 L 90 10 L 90 60 L 10 60 Z a M 10 10 H 90 V 60 H 10 Z: de ocho números a cuatro.
Hay un uso menos obvio y bastante más valioso: H y V documentan la intención. En una d generada por código, ver un V garantiza que ese segmento es exactamente vertical, sin errores de redondeo en x. Con L, un valor que debería ser idéntico puede diferir en la sexta cifra decimal por el camino que ha seguido el cálculo, y el resultado es una línea que se ve de un píxel de ancho en unos sitios y de dos en otros, con un antialiasing sucio. Si la línea es vertical por construcción, dilo con V.
Y una consecuencia de la que se habla poco: como H y V tienen un solo parámetro, la repetición implícita produce zigzags degenerados. H 10 20 30 traza tres segmentos horizontales consecutivos, es decir, uno solo de 10 a 30 pasando dos veces por sitios intermedios. No es un error, es inútil. Los optimizadores colapsan esos casos.
Z no es un segmento más
Z cierra la subruta trazando un segmento recto desde el punto actual hasta el punto inicial de la subruta. Parece equivalente a poner un L con las coordenadas de inicio, y no lo es.
La diferencia está en cómo se trata ese vértice al aplicar el trazo. Con Z hay una unión (stroke-linejoin). Sin Z, hay dos terminaciones (stroke-linecap) superpuestas: la del final del último segmento y la del principio del primero.
<svg viewBox="0 0 200 90" width="400">
<!-- Cerrado de verdad: esquina limpia -->
<path d="M 20 20 H 80 V 70 H 20 Z"
fill="none" stroke="#a6e3a1" stroke-width="12" stroke-linejoin="miter" />
<!-- Vuelta a mano: muesca en la esquina superior izquierda -->
<path d="M 120 20 H 180 V 70 H 120 L 120 20"
fill="none" stroke="#f38ba8" stroke-width="12" stroke-linejoin="miter" />
</svg>
Con doce unidades de grosor la diferencia es un mordisco evidente en la esquina superior izquierda del segundo. Con dos unidades no se ve. Con stroke-linecap="round" se ve poco, porque las dos terminaciones redondas rellenan el hueco casi del todo. Por eso el bug aparece de golpe el día que alguien cambia el grosor o el estilo de terminación de un componente entero.
Hay una segunda diferencia, esta invisible pero medible: Z marca la subruta como cerrada, y eso afecta a getTotalLength(), al comportamiento de stroke-dasharray (el patrón de guiones se envuelve alrededor del cierre) y a cómo el rasterizador determina el interior para el relleno. Una forma abierta se rellena igualmente, cerrándose implícitamente para el cálculo del relleno, pero el trazo no se cierra.
Después de Z, el punto actual pasa a ser el punto inicial de la subruta cerrada. Si a continuación viene un comando de dibujo sin un M nuevo, ese comando abre una subruta nueva que empieza exactamente ahí. Es decir, M 10 10 H 50 Z L 30 40 dibuja el segmento de (10,10) a (50,10), lo cierra, y luego traza otro segmento independiente de (10,10) a (30,40). Mucha gente asume que después de Z el path ha terminado y que lo que sigue es un error. No lo es, y esa es la razón por la que un path mal editado a mano a veces produce un segmento fantasma que sale del origen de la última pieza. Cuando depures una d, busca comandos de dibujo inmediatamente después de un Z.
Agujeros: fill-rule y el sentido de giro
Dos subrutas que se solapan plantean una pregunta: la región interior a ambas, ¿está dentro o fuera? La responde fill-rule, con dos valores.
nonzero (el valor por omisión) traza un rayo desde el punto hacia el infinito y cuenta los cruces con la ruta, sumando uno si el trazo cruza en un sentido y restando uno si cruza en el otro. Si el total es distinto de cero, el punto está dentro. La consecuencia: el sentido de giro importa. Dos círculos concéntricos dibujados en el mismo sentido dan un disco lleno; en sentidos opuestos, un anillo.
evenodd cuenta cruces sin signo: par significa fuera, impar dentro. El sentido de giro es irrelevante y dos círculos concéntricos siempre dan un anillo.
<svg viewBox="0 0 260 120" width="520">
<!-- nonzero, mismo sentido: disco lleno -->
<path fill="#89b4fa" d="M 60 20 A 40 40 0 1 0 60 100 A 40 40 0 1 0 60 20 Z
M 60 40 A 20 20 0 1 0 60 80 A 20 20 0 1 0 60 40 Z" />
<!-- evenodd, mismas subrutas: anillo -->
<path fill="#a6e3a1" fill-rule="evenodd"
d="M 190 20 A 40 40 0 1 0 190 100 A 40 40 0 1 0 190 20 Z
M 190 40 A 20 20 0 1 0 190 80 A 20 20 0 1 0 190 40 Z" />
</svg>
El primero sale relleno del todo, el segundo con agujero. Ambos tienen exactamente las mismas subrutas.
Qué regla elegir depende del origen del dato. Las tipografías y los exportadores de Illustrator suelen producir contornos con sentidos de giro coherentes, así que nonzero funciona y es más rápida de evaluar. Los datos geográficos y las formas generadas por código rara vez controlan el sentido, y ahí evenodd es la apuesta segura. Si un icono con agujeros te sale macizo, cambiar fill-rule a evenodd es la primera cosa que hay que probar; si eso lo arregla, sabes que el problema era el sentido de giro y no la geometría.
Un apunte: clip-rule es la propiedad hermana que hace exactamente lo mismo dentro de un clipPath. Se olvida constantemente, y produce recortes macizos donde esperabas un anillo.
Dibuja el mismo anillo de tres maneras: con dos subrutas y evenodd, con dos subrutas de sentidos opuestos y nonzero, y con un solo circle con fill="none" y un stroke-width igual al grosor del anillo. Compara el peso, y después comprueba qué pasa con cada uno cuando le aplicas un stroke adicional de color distinto. Solo dos de los tres te permiten poner borde al agujero.