El arco elíptico: A y sus dos banderas
Los siete parámetros del comando A, qué significan large-arc-flag y sweep-flag, la corrección automática de radios fuera de rango, y por qué un círculo completo necesita dos arcos.
A es el comando más denso del mini-lenguaje: siete parámetros, dos de los cuales son banderas booleanas que casi nadie sabe interpretar sin mirar la documentación. Y es el único comando que puede recibir valores imposibles y aun así dibujar algo, porque la especificación define una corrección automática. Entenderlo bien resuelve de un golpe los arcos de un gráfico de tarta, los sectores de un donut y las esquinas redondeadas escritas a mano.
- Escribir un
Acompleto y nombrar los siete parámetros en orden. - Elegir el valor de
large-arc-flagysweep-flagpara obtener uno de los cuatro arcos posibles. - Aplicar las tres reglas de corrección de radios fuera de rango de la especificación.
- Construir un sector de anillo completo, del tipo que necesita un gráfico de donut.
Los siete parámetros
<path d="A rx ry rotacion-eje-x bandera-arco-grande bandera-barrido x y" />
En orden: los dos radios de la elipse; la rotación del eje x de la elipse en grados; las dos banderas, cada una 0 o 1; y el punto final del arco.
Lo primero que hay que asimilar es que A es un comando de punto final, no de centro y ángulos. No dices «dibuja 90 grados de una circunferencia de radio 40 centrada aquí»; dices «llega hasta este punto siguiendo una elipse de estos radios». El motor calcula el centro. Esa parametrización, que se llama endpoint parameterization, es la que hace que A se pueda encadenar con el resto de comandos sin romper el modelo del punto actual, y es también la razón de las dos banderas.
El problema geométrico es este: dados dos puntos y dos radios, hay en general cuatro arcos elípticos distintos que los unen. Dos elipses candidatas (una a cada lado de la recta que une los puntos), y en cada una, dos maneras de ir de A a B (por el trozo corto o por el largo). Las banderas eligen entre esos cuatro.
large-arc-flag: 0 elige el arco de menos de 180 grados, 1 el de más de 180.
sweep-flag: 0 recorre en sentido antihorario (ángulo decreciente), 1 en sentido horario (ángulo creciente). En el sistema de coordenadas de SVG, con la y hacia abajo, «horario» es lo que se ve horario en pantalla.
<svg viewBox="0 0 240 130" width="480">
<g fill="none" stroke-width="3">
<path d="M 30 100 A 50 50 0 0 0 110 100" stroke="#89b4fa" />
<path d="M 30 100 A 50 50 0 0 1 110 100" stroke="#a6e3a1" />
<path d="M 30 100 A 50 50 0 1 0 110 100" stroke="#f9e2af" />
<path d="M 30 100 A 50 50 0 1 1 110 100" stroke="#f38ba8" />
</g>
</svg>
Cuatro d idénticas salvo por dos dígitos, cuatro arcos completamente distintos. Cuando un arco te sale «al revés» o «por el otro lado», el arreglo son esos dos dígitos, y probar las cuatro combinaciones cuesta diez segundos.
La rotación del eje x solo tiene efecto si rx y ry son distintos: rotar una circunferencia no hace nada. Con radios distintos, inclina la elipse. Se mide en grados y en el sentido positivo del sistema de coordenadas, es decir, horario en pantalla.
Radios fuera de rango
La especificación define qué hacer cuando los valores no tienen sentido geométrico, y esas reglas son la razón de que A casi nunca falle del todo.
Regla uno: si rx o ry es cero, el arco se trata como un segmento recto hasta el punto final. Ni error ni forma degenerada: una L. Esto es útil de verdad, porque permite generar una d con un radio variable y que el caso cero produzca la esquina recta sin ramificar el código.
Regla dos: los radios negativos se toman en valor absoluto. Un -40 es un 40.
Regla tres: si los radios son demasiado pequeños para que exista una elipse que pase por los dos puntos, se escalan hacia arriba de forma uniforme hasta que exista exactamente una solución. La especificación da la fórmula exacta en su apéndice de notas de implementación. En la práctica esto significa que un A 1 1 0 0 1 100 0 no falla: dibuja el semicírculo de radio 50 que sí cabe. Es la explicación de por qué un arco pequeño «no obedece» a los radios que le pones: está en el régimen de corrección.
Regla cuatro: si el punto final coincide con el punto inicial, el arco se omite por completo. No se dibuja nada, y el comando se descarta como si no estuviera.
Esa última regla es la respuesta a la pregunta que todo el mundo se hace al menos una vez.
Por qué un círculo necesita dos arcos
Un círculo completo empieza y acaba en el mismo punto. Por la regla cuatro, un solo A con inicio y fin coincidentes se descarta y no dibuja nada. La solución estándar son dos arcos de media vuelta:
<!-- Circulo de radio 40 centrado en (50, 50) -->
<path d="M 10 50 A 40 40 0 1 0 90 50 A 40 40 0 1 0 10 50 Z" />
De (10, 50) a (90, 50) por arriba, y de vuelta por abajo. Nótese que ambos usan large-arc-flag a 1, aunque cada uno sea exactamente medio círculo: cuando los puntos son diametralmente opuestos, los dos arcos posibles miden 180 grados y la bandera del arco grande deja de discriminar, así que da igual el valor. Lo que sí importa es que sweep-flag sea el mismo en los dos, para que el recorrido sea coherente.
Esto tiene una consecuencia directa en gráficos de tarta: un sector que abarca los 360 grados completos no se puede dibujar con la misma función que los demás. Si una categoría representa el cien por cien, la fórmula genérica produce inicio igual a fin y el sector desaparece. Hay que detectar ese caso y emitir un circle, o dos arcos. Es un bug clásico que solo aparece cuando alguien filtra los datos hasta dejar una sola categoría.
Un sector de anillo completo
Esta es la construcción que justifica todo el nivel: el trozo de donut, con radio interior y exterior, que es la unidad básica de media docena de gráficos.
// Sector de anillo. Angulos en radianes, 0 = tres en punto, creciente horario.
function sectorAnillo(cx, cy, rInt, rExt, a0, a1) {
const p = (r, a) => [cx + r * Math.cos(a), cy + r * Math.sin(a)];
const grande = Math.abs(a1 - a0) > Math.PI ? 1 : 0;
const [x0, y0] = p(rExt, a0);
const [x1, y1] = p(rExt, a1);
const [x2, y2] = p(rInt, a1);
const [x3, y3] = p(rInt, a0);
return [
`M ${x0} ${y0}`,
`A ${rExt} ${rExt} 0 ${grande} 1 ${x1} ${y1}`,
`L ${x2} ${y2}`,
`A ${rInt} ${rInt} 0 ${grande} 0 ${x3} ${y3}`,
'Z',
].join(' ');
}
// Un sector del 30 por ciento empezando arriba
const inicio = -Math.PI / 2;
sectorAnillo(100, 100, 40, 70, inicio, inicio + 0.3 * 2 * Math.PI);
Fíjate en los dos detalles que hacen que funcione. El sweep-flag del arco exterior es 1 y el del interior es 0, porque el recorrido de vuelta va en sentido contrario; si pones el mismo en los dos, la forma se cruza sobre sí misma y sale una mariposa. Y el large-arc-flag se calcula una vez y se usa en ambos, porque los dos arcos abarcan el mismo ángulo.
El cálculo del centro a partir de la parametrización por extremos implica una raíz cuadrada de una diferencia de cuadrados. Cuando el arco es casi un semicírculo, esa diferencia tiende a cero y el resultado pierde precisión: es una cancelación catastrófica de manual. Los motores lo gestionan con la corrección de radios, pero tú puedes provocarlo tú solo si redondeas las coordenadas antes de escribirlas en la d.
El caso concreto: generas un sector de anillo, redondeas a dos decimales para ahorrar bytes, y en un ángulo determinado los dos extremos del arco quedan a una distancia ligerísimamente mayor que el diámetro que permiten los radios. Entra la regla tres, los radios se escalan, y ese sector sale un pelín más gordo que sus vecinos. En una tarta de veinte sectores se ve como un diente irregular que aparece y desaparece al cambiar los datos.
La defensa tiene dos partes: calcula siempre en doble precisión y redondea solo al escribir, y no bajes de tres decimales en geometría con arcos. La compresión posterior recupera casi todo lo que crees que estás ahorrando con dos.
Escribe la función inversa: dado un A con su parametrización por extremos, devuelve el centro de la elipse, el ángulo inicial y el barrido. Compara tu resultado con lo que el navegador dibuja muestreando la ruta con getPointAtLength() en varios valores. Necesitarás la fórmula del apéndice de la especificación; el ejercicio real es descubrir en qué punto de esa fórmula aparece la raíz cuadrada que puede volverse negativa por error de redondeo, y qué hay que hacer con ella.