El modelo de path: la ruta actual y sus subrutas
Entender la ruta actual como estado oculto del contexto, cómo se construye con moveTo y lineTo, qué son las subrutas y qué hace exactamente closePath.
Construir y consumir
El modelo tiene dos fases separadas y esa separación es deliberada.
Primero construyes: beginPath descarta la ruta que hubiera y empieza una nueva; moveTo sitúa el lápiz sin trazar; lineTo, arc, bezierCurveTo y compañía añaden segmentos.
Después consumes: fill rellena la ruta, stroke traza su contorno, clip la convierte en región de recorte, isPointInPath pregunta si un punto está dentro.
Lo que sorprende es que consumir no destruye la ruta. Después de un fill, la ruta sigue exactamente igual, y puedes hacer stroke a continuación para obtener relleno y contorno de la misma forma. Eso es útil y deseado:
ctx.beginPath();ctx.moveTo(60, 20);ctx.lineTo(110, 100);ctx.lineTo(10, 100);ctx.closePath();ctx.fillStyle = '#89b4fa';ctx.fill(); // rellenactx.strokeStyle = '#1e1e2e';ctx.lineWidth = 4;ctx.stroke(); // traza la MISMA ruta, sin reconstruirlaEse es el patrón correcto para una forma con borde. Reconstruir la ruta entre el relleno y el trazado es trabajo duplicado y además puede producir diferencias de redondeo.
Subrutas
Una ruta puede contener varias subrutas, que son secuencias de segmentos conectados. Cada moveTo cierra la subruta en curso y abre una nueva.
ctx.beginPath();ctx.moveTo(20, 20); ctx.lineTo(80, 20); // subruta 1ctx.moveTo(20, 50); ctx.lineTo(80, 50); // subruta 2ctx.moveTo(20, 80); ctx.lineTo(80, 80); // subruta 3ctx.stroke(); // traza las tres de una vezEso son tres líneas independientes en una sola ruta y un solo stroke. Es más rápido que tres rutas con tres trazados, porque cada llamada a stroke tiene un coste fijo de preparación que aquí se paga una vez.
Las subrutas también son la base de las formas con agujeros. Un anillo es una ruta con dos subrutas circulares, y qué queda relleno lo decide la regla de relleno, que tiene su propia lección.
Hay una peculiaridad que conviene conocer: algunos métodos añaden implícitamente un moveTo. rect, roundRect, ellipse y arc en ciertas condiciones abren su propia subruta. Y hay una asimetría: rect deja el punto actual en su esquina inicial y cierra la subruta, mientras que arc conecta con una línea recta desde el punto actual si ya había uno.
closePath no es lo que parece
closePath cierra la subruta actual añadiendo un segmento recto desde el punto actual hasta el punto inicial de esa subruta, y marca la subruta como cerrada. Esa segunda parte es la que importa y la que se ignora.
Comparemos:
// A: cerrar con closePathctx.beginPath();ctx.moveTo(30, 30); ctx.lineTo(90, 30); ctx.lineTo(60, 85);ctx.closePath();ctx.lineWidth = 12; ctx.strokeStyle = '#a6e3a1';ctx.stroke();// B: "cerrar" volviendo al inicio con lineToctx.beginPath();ctx.moveTo(160, 30); ctx.lineTo(220, 30); ctx.lineTo(190, 85);ctx.lineTo(160, 30);ctx.stroke();Los dos triángulos tienen la misma geometría, pero el vértice superior izquierdo se ve distinto. En el caso A hay una unión de línea, con el remate que dicte lineJoin. En el caso B hay dos extremos de línea que se solapan, con el remate que dicte lineCap, y con lineCap plano —el valor por defecto— aparece una muesca en la esquina.
Con grosores finos la diferencia es imperceptible; con grosores gruesos es escandalosa. Es la causa habitual de “mis polígonos tienen una esquina rota”.
Para el relleno, en cambio, la diferencia no existe: fill cierra implícitamente todas las subrutas abiertas antes de rellenar, porque una región sin cerrar no tendría sentido.
Un tercer matiz: después de closePath, el punto actual vuelve al inicio de la subruta cerrada, y el siguiente lineTo continúa desde ahí en la misma subruta. No abre una nueva. Si quieres una subruta nueva después de cerrar, necesitas un moveTo.
Leer una secuencia sin ejecutarla
La habilidad práctica que hay que desarrollar es seguir mentalmente el estado de la ruta. Estas son las reglas completas, y con ellas se puede predecir cualquier secuencia.
| Llamada | Efecto sobre la ruta | Punto actual después |
|---|---|---|
beginPath() |
Descarta todo | Indefinido |
moveTo(x, y) |
Cierra la subruta en curso, abre una nueva | (x, y) |
lineTo(x, y) |
Añade segmento recto; si no había punto actual, actúa como moveTo |
(x, y) |
closePath() |
Cierra la subruta con un segmento al inicio | Inicio de la subruta |
rect(...) |
Añade una subruta cerrada completa y abre otra nueva vacía | Esquina inicial |
arc(...) |
Conecta con línea recta desde el punto actual, si lo hay | Fin del arco |
fill() / stroke() |
No modifica la ruta | Sin cambios |
La fila de lineTo sin punto actual es una salvaguarda de la especificación que evita errores, pero apoyarse en ella hace el código ilegible.
Un ejercicio de lectura:
ctx.beginPath();ctx.rect(20, 20, 60, 40);ctx.lineTo(120, 100);ctx.stroke();¿Qué dibuja? El rect añade un rectángulo cerrado y deja la ruta con una subruta nueva y vacía posicionada en (20, 20). El lineTo añade a esa subruta vacía un segmento desde (20, 20) hasta (120, 100). El resultado es un rectángulo más una diagonal que sale de su esquina superior izquierda. Casi nadie lo predice a la primera.
Una utilidad para polígonos
Con lo anterior ya se puede escribir la función de propósito general que acaba en todos los proyectos:
/** Traza un poligono a partir de una lista de puntos. */function poligono(ctx, puntos, cerrar = true) { if (puntos.length === 0) return; ctx.beginPath(); ctx.moveTo(puntos[0].x, puntos[0].y); for (let i = 1; i < puntos.length; i++) { ctx.lineTo(puntos[i].x, puntos[i].y); } if (cerrar) ctx.closePath();}/** Poligono regular de n lados centrado en (cx, cy). */function regular(ctx, cx, cy, radio, n, giro = -Math.PI / 2) { const puntos = Array.from({ length: n }, (_, i) => { const a = giro + (i * 2 * Math.PI) / n; return { x: cx + radio * Math.cos(a), y: cy + radio * Math.sin(a) }; }); poligono(ctx, puntos);}// Usoregular(ctx, 120, 120, 80, 6);ctx.fillStyle = '#cba6f7'; ctx.fill();ctx.strokeStyle = '#11111b'; ctx.lineWidth = 3; ctx.stroke();Fíjate en el giro por defecto de menos noventa grados: sin él, el primer vértice cae a la derecha y los polígonos de lados impares salen apoyados en un vértice en lugar de en un lado, que es lo que nadie espera.