Saltar al contenido

circle y ellipse: el radio y sus trampas

Por qué SVG habla de radios y no de diámetros, contra qué se resuelve un porcentaje en r, el valor auto de rx y ry en la elipse, y el coste de aproximar un círculo con curvas.

Centro y radio, nunca caja

Un circle tiene tres atributos y ninguno más: cx, cy y r. Una ellipse tiene cuatro: cx, cy, rx y ry. Todos con valor inicial cero.

HTML
<svg viewBox="0 0 200 100" width="400">  <circle  cx="50"  cy="50" r="40"           fill="#f38ba8" />  <ellipse cx="140" cy="50" rx="50" ry="30"  fill="#94e2d5" /></svg>

La decisión de posicionar por centro y no por caja envolvente es la primera diferencia mental con CSS, y no es un capricho. Un círculo definido por su centro rota sobre sí mismo con un transform sin corrección; definido por su esquina superior izquierda, cualquier rotación exige recalcular el origen. Como SVG nació para gráficos técnicos donde las rotaciones son constantes, el centro era la parametrización natural. La consecuencia práctica es que centrar un círculo en un lienzo es trivial (cx y cy a la mitad) mientras que centrar un rect obliga a restar la mitad del ancho.

La segunda diferencia es que se habla de radio, no de diámetro. Un punto de datos de 12 píxeles de diámetro se escribe r="6". Suena obvio hasta que generas una nube de puntos desde datos y todos salen del doble de tamaño.

Un r negativo es un error de valor y el elemento no se renderiza. Un r de cero es válido y desactiva el renderizado del elemento, que es exactamente lo que quieres cuando una escala te devuelve cero: la forma desaparece sin ensuciar el DOM ni lanzar nada.

El porcentaje y la diagonal normalizada

Este es el apartado que hay que leer despacio. Cuando un valor de longitud es un porcentaje, SVG lo resuelve contra el viewport actual, pero elige la referencia según el eje de esa longitud:

  • Longitud horizontal (cx, rx, x, width): porcentaje del ancho del viewport.
  • Longitud vertical (cy, ry, y, height): porcentaje del alto del viewport.
  • Longitud sin eje (r, stroke-width, stroke-dashoffset, font-size): porcentaje de la diagonal normalizada.

La diagonal normalizada se define así, con w y h las dimensiones del viewport:

JS
const diagonalNormalizada = Math.sqrt(w * w + h * h) / Math.SQRT2;

Es la longitud de la diagonal dividida por raíz de dos. En un viewport cuadrado coincide con el lado, y por eso en un lienzo cuadrado los porcentajes de r parecen comportarse «bien» y nadie nota nada. En un viewport de 400 por 100, en cambio, la diagonal normalizada vale unos 291,5, así que r="50%" son 145,7 unidades: un círculo que desborda el alto por completo. Ese es el bug, y no tiene arreglo salvo dejar de usar porcentajes en r.

La lección operativa es simple: en r usa unidades de usuario, no porcentajes. Si necesitas un círculo relativo al contenedor, el sitio donde se resuelve la proporción es el viewBox, no el radio.

auto en la elipse

ellipse comparte con rect el mecanismo de auto. rx y ry valen auto por omisión, y auto significa «el valor usado del otro». De modo que:

HTML
<svg viewBox="0 0 300 100" width="600">  <!-- Circulo de radio 30 escrito como elipse -->  <ellipse cx="50"  cy="50" rx="30"          fill="#89b4fa" />  <!-- Identico: ry copia rx -->  <ellipse cx="150" cy="50" ry="30"          fill="#89b4fa" />  <!-- Elipse de verdad -->  <ellipse cx="250" cy="50" rx="45" ry="20"  fill="#cba6f7" /></svg>

Si ambos son auto, ambos valen cero y no se pinta nada. No hay recorte aquí, a diferencia de rect: una elipse no tiene ninguna caja contra la que limitarse, así que rx y ry se usan tal cual y la forma puede desbordar el viewport sin problema. Lo que desborda simplemente se recorta contra el viewport si el overflow del elemento raíz lo dice, que es lo habitual.

Merece la pena notar que ellipse con rx igual a ry y circle con ese mismo radio producen exactamente el mismo rasterizado. No hay una ruta de código más rápida para el círculo. La única razón para preferir circle es semántica y de tamaño de fichero: un atributo menos y una intención más clara para quien lea el documento.

Círculo nativo frente a círculo con curvas

Ninguna curva de Bézier puede describir un arco de circunferencia de forma exacta: la circunferencia es una cónica y la Bézier es un polinomio. Lo que hacen todas las herramientas es aproximar cada cuarto de círculo con una cúbica cuyos puntos de control están a distancia k · r del extremo, con la constante mágica:

JS
// Constante de aproximacion de un cuarto de circulo con una cubicaconst k = 4 / 3 * (Math.SQRT2 - 1); // 0.5522847498307933

Con cuatro cúbicas así, el error máximo respecto al círculo verdadero es del orden de 2 diezmilésimas del radio. Invisible en pantalla, irrelevante para impresión, y perfectamente medible si haces cálculos geométricos sobre la forma.

Eso significa que un circle de SVG no se aproxima: el rasterizador dibuja la cónica directamente y es exacto a resolución de subpíxel. Un path con las cuatro cúbicas es una aproximación. En la práctica visual da igual, pero hay dos consecuencias que no dan igual:

El peso. Un <circle cx="50" cy="50" r="40"/> son unos 35 bytes. El mismo círculo como path con cuatro cúbicas y tres decimales pasa de 120. Multiplicado por los cientos de puntos de una nube de datos, la diferencia es real. Por eso el plugin convertShapeToPath de SVGO deja los círculos en paz por omisión, y por eso hay que revisar qué hace tu herramienta de exportación: Illustrator convierte círculos a paths con frecuencia y Figma también, según cómo se haya construido la forma.

El punto de partida del trazo. Un circle empieza su recorrido en el punto de las tres en punto y avanza en sentido horario. Un path empieza donde tú digas. Eso importa en el momento en que aplicas stroke-dasharray para dibujar un anillo de progreso: la posición de la marca del cero depende de dónde arranque la ruta, y con un circle tienes que rotar la forma noventa grados para que el progreso empiece arriba.

HTML
<svg viewBox="0 0 120 120" width="240">  <circle cx="60" cy="60" r="50" fill="none" stroke="#313244" stroke-width="10" />  <circle cx="60" cy="60" r="50" fill="none" stroke="#a6e3a1" stroke-width="10"          stroke-linecap="round"          stroke-dasharray="314.159"          stroke-dashoffset="94.248"          transform="rotate(-90 60 60)" /></svg>

La circunferencia de radio 50 mide 2 · Math.PI * 50, es decir 314,159. Un desfase de 94,248 deja fuera el 30 por ciento, así que el arco visible representa el 70 por ciento. La rotación de menos noventa grados alrededor del centro traslada el origen de las tres en punto a las doce.