Saltar al contenido

rect, roundRect y ellipse: las primitivas con nombre

Usar las primitivas de ruta que el canvas ofrece hechas, conocer sus reglas de sentido y de radios, y saber cuándo compensa frente a construir la forma a mano.

rect: la primitiva base

JS
ctx.rect(x, y, ancho, alto);

Añade a la ruta una subruta rectangular ya cerrada y, a continuación, abre una subruta nueva y vacía posicionada en (x, y). Ese segundo efecto es el que sorprende y el que explica el resultado extraño de encadenar un lineTo después de un rect.

El sentido de recorrido con ancho y alto positivos es horario: desde la esquina superior izquierda hacia la derecha, hacia abajo, hacia la izquierda y hacia arriba. Con valores negativos, el rectángulo se dibuja igual pero el sentido se invierte, lo que tiene una aplicación directa.

JS
// Marco: rectangulo exterior horario, interior antihorario -> agujeroctx.beginPath();ctx.rect(20, 20, 200, 140);ctx.rect(220, 160, -160, -100);   // dimensiones negativas: sentido inversoctx.fillStyle = '#94e2d5';ctx.fill();                        // regla nonzero: el interior queda vacio

Ese truco de las dimensiones negativas funciona con la regla de relleno por defecto y es más elegante que la alternativa de recorrer el rectángulo interior a mano con cuatro lineTo en orden inverso. Con la regla evenodd ni siquiera hace falta invertir nada.

rect no debe confundirse con fillRect y strokeRect, que dibujan de inmediato y no tocan la ruta actual. La regla es sencilla: si vas a hacer algo más con la forma —recortar, combinar con otras, preguntar si un punto está dentro— usa rect y una ruta; si solo quieres pintar un rectángulo suelto, fillRect es más directo y algo más rápido.

roundRect: radios por esquina

JS
ctx.roundRect(x, y, ancho, alto, radios);

El último parámetro admite varias formas, y sigue las mismas reglas que la propiedad border-radius de CSS:

JS
ctx.roundRect(x, y, w, h, 12);                  // los cuatro radios igualesctx.roundRect(x, y, w, h, [12]);                // igual que el anteriorctx.roundRect(x, y, w, h, [12, 0]);             // sup-izq y inf-der = 12; los otros 0ctx.roundRect(x, y, w, h, [12, 0, 24]);         // sup-izq, [sup-der + inf-izq], inf-derctx.roundRect(x, y, w, h, [12, 0, 24, 6]);      // los cuatro, en sentido horario

El orden de los cuatro es superior izquierda, superior derecha, inferior derecha, inferior izquierda: el mismo que en CSS y el mismo sentido horario que recorre el rectángulo.

Cada radio puede ser también un objeto DOMPointInit con x e y distintos, lo que da esquinas elípticas:

JS
ctx.beginPath();ctx.roundRect(20, 20, 240, 120, [{ x: 60, y: 20 }, 8, 8, { x: 60, y: 20 }]);ctx.fillStyle = '#fab387';ctx.fill();

Hay dos reglas de saneamiento que evitan sorpresas. Si la suma de los radios de un lado supera la longitud de ese lado, todos los radios se escalan proporcionalmente hasta que quepan, exactamente como hace CSS. Y un radio negativo lanza un RangeError, así que si los radios vienen de un cálculo, protégelos.

roundRect está disponible en los tres motores desde 2022-2023 y es hoy la forma correcta de hacer un rectángulo redondeado. La construcción con arcTo sigue siendo válida y sigue siendo necesaria en cuanto la forma deja de ser un rectángulo.

ellipse: el círculo generalizado

JS
ctx.ellipse(cx, cy, radioX, radioY, rotacion, anguloInicio, anguloFin, antihorario = false);

Es arc con dos radios y un ángulo de rotación del eje. La rotación va en radianes y gira la elipse alrededor de su centro; los ángulos de inicio y fin se miden en el sistema girado, lo cual es lo que quieres pero conviene saberlo.

Igual que arc, conecta con una línea recta desde el punto actual si lo hay, así que necesita el mismo moveTo defensivo cuando dibujas varias.

JS
function elipse(ctx, cx, cy, rx, ry, rot = 0) {  ctx.moveTo(cx + rx * Math.cos(rot), cy + rx * Math.sin(rot));  ctx.ellipse(cx, cy, rx, ry, rot, 0, Math.PI * 2);}ctx.beginPath();elipse(ctx, 120, 100, 90, 45, -Math.PI / 8);ctx.fillStyle = '#f5c2e7';ctx.fill();

Un radio negativo lanza IndexSizeError; un radio cero es válido y produce un segmento degenerado.

Cuándo la primitiva y cuándo la construcción a mano

La primitiva gana siempre que la forma sea exactamente lo que la primitiva describe. Es más corto, más legible y menos propenso a errores de signo.

La construcción a mano es necesaria en tres casos.

Cuando la forma no es la primitiva. Un bocadillo con pico, una tarjeta con una esquina cortada en diagonal, una pestaña. Aquí se recorre el contorno con lineTo y se redondean los vértices con arcTo.

Cuando necesitas control sobre el sentido. Las primitivas tienen un sentido fijo salvo por el truco de las dimensiones negativas. Si construyes una forma con varios agujeros anidados y quieres decidir el sentido de cada contorno, hazlo a mano o usa la regla evenodd.

Cuando necesitas los puntos. Si además de dibujar la forma vas a necesitar sus vértices —para animarlos, para hacer hit testing exacto, para exportarla— es mejor tener la lista de puntos como dato y una función que la traza, porque entonces la ruta se deriva del dato y no al revés.

JS
// Tarjeta con esquina cortada: no hay primitiva, y se construye en seis lineasfunction tarjetaCortada(ctx, x, y, w, h, corte = 22, r = 10) {  ctx.beginPath();  ctx.moveTo(x + r, y);  ctx.lineTo(x + w - corte, y);  ctx.lineTo(x + w, y + corte);  ctx.arcTo(x + w, y + h, x, y + h, r);  ctx.arcTo(x, y + h, x, y, r);  ctx.arcTo(x, y, x + w, y, r);  ctx.closePath();}