Saltar al contenido

Arcos: arc, arcTo y el modelo de las tangentes

Dominar los dos métodos de arco del canvas, sus modelos completamente distintos, y usarlos para círculos, sectores y esquinas redondeadas correctas.

arc: centro, radio y ángulos

JS
ctx.arc(cx, cy, radio, anguloInicio, anguloFin, antihorario = false);

Los ángulos van en radianes, no en grados, y el cero apunta hacia la derecha, es decir, en la dirección del eje X positivo. Como el eje Y crece hacia abajo, los ángulos crecientes recorren el círculo en el sentido de las agujas del reloj visualmente.

JS
// Circulo completoctx.beginPath();ctx.arc(100, 100, 60, 0, Math.PI * 2);ctx.fillStyle = '#f9e2af';ctx.fill();// Sector de 0 a 90 grados: cuarto INFERIOR derechoctx.beginPath();ctx.moveTo(250, 100);                          // al centro, para cerrar el sectorctx.arc(250, 100, 60, 0, Math.PI / 2);ctx.closePath();ctx.fillStyle = '#f38ba8';ctx.fill();

El detalle que hay que tener siempre presente: arc conecta con una línea recta desde el punto actual hasta el inicio del arco si ya había un punto actual. Es un comportamiento útil —permite el sector del ejemplo, donde el moveTo al centro genera el radio— y es también la fuente del bug más común con arcos.

JS
// Bug clasico: dos circulos unidos por una rayactx.beginPath();ctx.arc(60, 60, 30, 0, Math.PI * 2);ctx.arc(160, 60, 30, 0, Math.PI * 2);   // conecta desde el final del primeroctx.stroke();// Correcto: un moveTo antes de cada arco, o rutas separadasctx.beginPath();ctx.moveTo(90, 60);  ctx.arc(60, 60, 30, 0, Math.PI * 2);ctx.moveTo(190, 60); ctx.arc(160, 60, 30, 0, Math.PI * 2);ctx.stroke();

El moveTo va al punto de inicio del arco, que con ángulo inicial cero es (cx + radio, cy). Escribirlo a mano es tedioso y por eso conviene una utilidad:

JS
function circulo(ctx, cx, cy, r) {  ctx.moveTo(cx + r, cy);  ctx.arc(cx, cy, r, 0, Math.PI * 2);}

El sexto parámetro invierte el sentido del recorrido. Visualmente puede dar el mismo arco, pero el sentido importa para dos cosas: la regla de relleno nonzero, que decide qué es interior según la orientación de los contornos, y la dirección en la que se dibujan los guiones de una línea discontinua.

JS
// Anillo: exterior en un sentido, interior en el contrarioctx.beginPath();ctx.arc(120, 120, 80, 0, Math.PI * 2, false);ctx.arc(120, 120, 45, 0, Math.PI * 2, true);   // sentido inverso: agujeroctx.fillStyle = '#94e2d5';ctx.fill();

Ese anillo funciona con la regla de relleno por defecto precisamente por la inversión del sentido. Con los dos arcos en el mismo sentido, el círculo interior no sería un agujero y el resultado sería un disco lleno.

arcTo: dos rectas y un radio

JS
ctx.arcTo(x1, y1, x2, y2, radio);

Este método no recibe un centro ni ángulos. Recibe dos puntos y un radio, y hace lo siguiente: considera la recta que va del punto actual a (x1, y1) y la que va de (x1, y1) a (x2, y2), y traza el arco de radio dado que es tangente a ambas. Si hace falta, añade primero un segmento recto desde el punto actual hasta donde empieza el arco.

El modelo mental correcto es el de doblar una esquina: (x1, y1) es el vértice que quieres redondear, (x2, y2) indica hacia dónde sigue el camino, y el radio dice cuánto redondear.

Dos consecuencias que hay que fijar:

El arco no llega a (x1, y1). Ese punto es el vértice teórico, y el arco lo esquiva. El punto actual después de la llamada queda en la tangente con el segundo segmento, que está entre (x1, y1) y (x2, y2).

El arco tampoco llega a (x2, y2). Ese punto solo define la dirección. Si quieres llegar hasta él, necesitas un lineTo después.

JS
// Rectangulo con esquinas redondeadas usando arcTofunction rectRedondeado(ctx, x, y, w, h, r) {  ctx.beginPath();  ctx.moveTo(x + r, y);  ctx.arcTo(x + w, y,     x + w, y + h, r);   // esquina superior derecha  ctx.arcTo(x + w, y + h, x,     y + h, r);   // inferior derecha  ctx.arcTo(x,     y + h, x,     y,     r);   // inferior izquierda  ctx.arcTo(x,     y,     x + w, y,     r);   // superior izquierda  ctx.closePath();}rectRedondeado(ctx, 20, 20, 220, 120, 24);ctx.fillStyle = '#a6e3a1';ctx.fill();

Fíjate en el moveTo(x + r, y) inicial: empieza justo donde acabará el último arco, de modo que closePath no añada ningún segmento visible. Este patrón fue durante años la única forma de hacer un rectángulo redondeado, y todavía es el que hay que entender aunque ahora exista roundRect.

Los casos degenerados

Ambos métodos tienen situaciones límite con comportamiento definido, y conocerlas evita sorpresas.

arc con radio cero no lanza error: produce una subruta con un solo punto, que no se ve. Con radio negativo sí lanza un IndexSizeError. Si tus radios vienen de un cálculo, protégelos con Math.max(0, r).

arc con un rango de ángulos mayor que dos pi dibuja el círculo completo y se detiene; no da vueltas de más.

arcTo con radio cero degenera en un lineTo(x1, y1): no hay arco, hay esquina viva.

arcTo con los tres puntos colineales también degenera en una línea recta hasta (x1, y1), porque no hay esquina que redondear.

arcTo con un radio mayor del que cabe no lanza error ni recorta: coloca el arco de ese radio tangente a las dos rectas, lo que puede llevarlo muy lejos del vértice y producir una forma inesperada. Es la causa habitual de “mi rectángulo redondeado se convirtió en algo raro” cuando el radio supera la mitad del lado más corto. La defensa es limitarlo:

JS
const r = Math.min(radio, w / 2, h / 2);

Elegir entre los dos

La regla es directa y no tiene excepciones prácticas.

Usa arc cuando conozcas el centro: círculos, sectores, anillos, indicadores de progreso circulares, marcadores, cualquier cosa radial.

Usa arcTo cuando estés recorriendo un contorno y quieras redondear los vértices: cajas, bocadillos de diálogo, contornos poligonales suavizados, rutas con esquinas.

Y para el caso concreto del rectángulo redondeado existe hoy roundRect, que hace lo mismo con menos código y admite radios distintos por esquina. Sigue mereciendo la pena entender arcTo porque es la herramienta general: en cuanto la forma deja de ser un rectángulo, roundRect no sirve y arcTo sí.

JS
// Bocadillo con pico y esquinas redondeadasfunction bocadillo(ctx, x, y, w, h, r, pico = 14) {  ctx.beginPath();  ctx.moveTo(x + r, y);  ctx.arcTo(x + w, y, x + w, y + h, r);  ctx.arcTo(x + w, y + h, x, y + h, r);  ctx.lineTo(x + 42, y + h);  ctx.lineTo(x + 28, y + h + pico);           // el pico  ctx.lineTo(x + 26, y + h);  ctx.arcTo(x, y + h, x, y, r);  ctx.arcTo(x, y, x + w, y, r);  ctx.closePath();}