Saltar al contenido

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.

JS
['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.

JS
// 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:

JS
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/lineTo

lineJoin: 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.

JS
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.