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.
Además de los segmentos y las curvas, el modelo de rutas incluye tres primitivas cerradas que ahorran trabajo: el rectángulo, el rectángulo redondeado y la elipse. Las tres tienen reglas propias sobre el sentido del recorrido, sobre qué pasa con valores negativos y sobre cómo dejan el punto actual, y ninguna de esas reglas es evidente hasta que un anillo sale relleno o un radio explota.
- Usar
rect,roundRectyellipseconociendo su efecto sobre la ruta actual. - Aplicar la sintaxis completa de radios de
roundRect, incluidos los radios elípticos. - Explicar el sentido de recorrido de cada primitiva y su efecto en las formas con agujero.
- Decidir cuándo usar la primitiva y cuándo construir la forma a mano.
rect: la primitiva base
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.
// Marco: rectangulo exterior horario, interior antihorario -> agujero
ctx.beginPath();
ctx.rect(20, 20, 200, 140);
ctx.rect(220, 160, -160, -100); // dimensiones negativas: sentido inverso
ctx.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
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:
ctx.roundRect(x, y, w, h, 12); // los cuatro radios iguales
ctx.roundRect(x, y, w, h, [12]); // igual que el anterior
ctx.roundRect(x, y, w, h, [12, 0]); // sup-izq y inf-der = 12; los otros 0
ctx.roundRect(x, y, w, h, [12, 0, 24]); // sup-izq, [sup-der + inf-izq], inf-der
ctx.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:
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
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.
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.
Existe la intuición razonable de que roundRect debe ser más rápido que construir la misma forma con cuatro arcTo, porque “está implementado en C++”. En la práctica no es así, y la razón es interesante: todas estas primitivas se traducen internamente a segmentos y curvas del mismo modelo de rutas, así que la geometría resultante es idéntica y el coste de rasterización también. Lo que ahorras es el tiempo de las llamadas desde JavaScript, que en un bucle de miles de formas es real pero pequeño comparado con el rasterizado. Y hay un caso en el que la primitiva sale perdiendo: el saneamiento de parámetros. roundRect valida el formato del argumento de radios, que puede ser un número, un array de uno a cuatro elementos o un objeto con x e y, normaliza los cuatro valores y comprueba que quepan escalándolos si no. Eso es bastante trabajo por llamada, y en un bucle que dibuja diez mil tarjetas con el mismo radio se nota. La optimización que de verdad importa no es elegir entre roundRect y arcTo, sino construir la forma una vez como Path2D y reutilizarla, con lo que la geometría se calcula una sola vez y las siguientes llamadas solo la rasterizan en la posición que toque. Ese es el patrón que separa un canvas que dibuja mil elementos de uno que dibuja cien mil, y el objeto que lo permite tiene su propio nivel más adelante.
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.
// Tarjeta con esquina cortada: no hay primitiva, y se construye en seis lineas
function 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();
}