lineCap, lineJoin y miterLimit
Controlar cómo terminan y cómo se unen los trazos, entender por qué existe el límite de inglete y qué pasa cuando se supera.
lineCap: los extremos
Tres valores, y el que está por defecto no es el que la gente espera.
butt (por defecto): el trazo termina exactamente en el punto final. La línea mide justo lo que mide.
round: se añade un semicírculo de radio igual a la mitad del grosor. La línea sobresale medio grosor por cada extremo.
square: se añade un rectángulo del mismo ancho y de medio grosor de largo. Sobresale lo mismo que round pero con esquinas vivas.
['butt', 'round', 'square'].forEach((cap, i) => { ctx.lineCap = cap; ctx.lineWidth = 20; ctx.strokeStyle = '#89b4fa'; ctx.beginPath(); ctx.moveTo(40, 40 + i * 50); ctx.lineTo(200, 40 + i * 50); ctx.stroke(); // Marcas de referencia en los extremos exactos ctx.fillStyle = '#f38ba8'; ctx.fillRect(39, 34 + i * 50, 2, 12); ctx.fillRect(199, 34 + i * 50, 2, 12);});Con las marcas se ve el efecto: butt acaba en la marca, los otros dos la sobrepasan en diez píxeles.
Esa diferencia de longitud tiene consecuencias prácticas concretas. Un indicador de progreso circular con lineCap = 'round' empieza y acaba fuera de su ángulo teórico, así que al cien por cien el extremo final se solapa con el inicial. Y una barra de progreso con extremos redondeados nunca puede mostrar un cero real, porque el semicírculo ya ocupa espacio.
// Progreso circular con extremos redondeados: hay que compensar el desbordefunction progreso(ctx, cx, cy, r, fraccion, grosor = 14) { const compensacion = grosor / 2 / r; // el semicirculo en radianes const inicio = -Math.PI / 2 + compensacion; const total = Math.PI * 2 - compensacion * 2; ctx.lineCap = 'round'; ctx.lineWidth = grosor; ctx.strokeStyle = '#313244'; ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.stroke(); if (fraccion <= 0) return; ctx.strokeStyle = '#a6e3a1'; ctx.beginPath(); ctx.arc(cx, cy, r, inicio, inicio + total * Math.min(1, fraccion)); ctx.stroke();}Un detalle importante: lineCap afecta también a los guiones. Con round, cada guion es una cápsula y la separación efectiva se reduce en un grosor. Con un patrón de guiones cortos y un grosor grueso, round puede hacer que los guiones se toquen y la línea parezca continua.
Y otro que sorprende: con butt, una subruta de longitud cero no dibuja nada. Con round o square, dibuja un punto. Es la forma de dibujar puntos con un solo stroke:
ctx.lineCap = 'round';ctx.lineWidth = 8;ctx.beginPath();for (const p of puntos) { ctx.moveTo(p.x, p.y); ctx.lineTo(p.x, p.y); }ctx.stroke(); // un punto redondo por cada par moveTo/lineTolineJoin: las esquinas
Tres valores para decidir qué pasa donde dos segmentos se encuentran.
miter (por defecto): se prolongan los bordes exteriores hasta que se cortan, formando una punta.
round: se rellena la esquina con un arco de radio igual a la mitad del grosor.
bevel: se rellena con un triángulo que une los dos extremos exteriores, cortando la punta en recto.
Con ángulos abiertos los tres se parecen. La diferencia explota con ángulos cerrados, y de ahí sale el parámetro siguiente.
miterLimit: la púa que se dispara
Cuando dos segmentos forman un ángulo muy agudo, la punta del inglete se alarga enormemente. En el límite de un ángulo que tiende a cero, la punta tiende a infinito.
miterLimit es el cociente máximo permitido entre la longitud del inglete y el grosor de la línea. Su valor por defecto es 10, lo que significa que la punta puede llegar a medir diez veces el grosor antes de que el navegador intervenga.
Cuando se supera, la unión pasa a comportarse como bevel para esa esquina concreta. No se recorta la punta a la longitud del límite: se cambia de tipo de unión.
const angulos = [10, 20, 40, 90];angulos.forEach((grados, i) => { const a = (grados * Math.PI) / 180; const x = 60 + i * 90, y = 140; ctx.lineWidth = 14; ctx.lineJoin = 'miter'; ctx.miterLimit = 10; ctx.strokeStyle = '#f9e2af'; ctx.beginPath(); ctx.moveTo(x - 50 * Math.cos(a / 2), y - 50 * Math.sin(a / 2)); ctx.lineTo(x, y); ctx.lineTo(x - 50 * Math.cos(a / 2), y + 50 * Math.sin(a / 2)); ctx.stroke();});Con diez grados la punta es larguísima o ya ha saltado a bisel; con noventa es corta. La relación exacta es que la longitud del inglete dividida por el grosor vale 1 / sin(θ/2), donde θ es el ángulo interior. Con el límite por defecto de 10, el cambio a bisel ocurre alrededor de los 11,5 grados.
Combinaciones que funcionan
| Caso | lineCap | lineJoin | Comentario |
|---|---|---|---|
| Rejilla, ejes, separadores | butt |
irrelevante | Longitud exacta, sin desbordes |
| Gráfica de líneas con datos reales | round |
round |
Sin púas, extremos limpios |
| Trazo a mano alzada | round |
round |
Se siente como un rotulador |
| Contorno de polígono | butt |
miter |
Esquinas nítidas, ángulos controlados |
| Indicador de progreso | round |
irrelevante | Recuerda compensar el desborde |
| Flecha o guía técnica | butt |
miter |
Precisión sobre estética |
Y una advertencia sobre el estado: lineCap, lineJoin y miterLimit forman parte del estado del contexto y persisten hasta que los cambies. Una función que pone lineCap = 'round' para dibujar puntos y no lo restaura deja todas las líneas siguientes con extremos redondeados, y el síntoma —una rejilla cuyas líneas sobresalen unos píxeles— es lo bastante sutil como para pasar desapercibido durante meses.