Líneas discontinuas: setLineDash y lineDashOffset
Manejar el patrón de guiones del canvas, entender cómo se recorre a lo largo de la ruta y usar el desfase para animar y para dibujar trazos progresivos.
El patrón
ctx.setLineDash([12, 6]); // 12 unidades pintadas, 6 vacias, y repitectx.setLineDash([12, 6, 3, 6]); // guion largo, hueco, punto, huecoctx.setLineDash([]); // vuelve a linea continuactx.getLineDash(); // devuelve una copia del array actualEl array son longitudes alternas de trazo y hueco, en unidades del espacio actual, no en píxeles. Si el contexto está escalado por la densidad de pantalla, un patrón de [12, 6] mide doce y seis píxeles lógicos, que es lo que quieres.
Hay una regla de normalización: si el array tiene un número impar de elementos, se duplica. Así, [10] equivale a [10, 10], y [6, 3, 2] equivale a [6, 3, 2, 6, 3, 2]. Merece la pena conocerlo porque un patrón de tres elementos no hace lo que parece a simple vista.
Un array con valores negativos o no finitos se ignora por completo, dejando el patrón anterior intacto, con la misma trampa silenciosa que otras asignaciones del contexto.
ctx.strokeStyle = '#a6e3a1';ctx.lineWidth = 3;[[8, 8], [16, 4], [1, 5], [20, 5, 4, 5]].forEach((patron, i) => { ctx.setLineDash(patron); ctx.beginPath(); ctx.moveTo(20, 30 + i * 28); ctx.lineTo(300, 30 + i * 28); ctx.stroke();});ctx.setLineDash([]); // restaurar SIEMPRECómo se recorre
El patrón avanza a lo largo de la longitud acumulada de la subruta, no por segmento. Eso significa que en una polilínea, un guion puede empezar en un segmento y acabar en el siguiente, doblando la esquina. Es lo que quieres visualmente y es lo que hace que el patrón se vea uniforme en curvas.
Cada subruta reinicia el patrón. Una ruta con tres subrutas empieza el patrón tres veces, cada una en el desfase actual.
Ese detalle tiene una consecuencia práctica en rejillas: si dibujas todas las líneas de la rejilla en una sola ruta con moveTo/lineTo, cada línea arranca el patrón desde el principio y todas quedan alineadas. Si las dibujaras encadenadas, el patrón se desplazaría en cada una.
Y una consecuencia estética en formas cerradas: en un rectángulo con guiones, el patrón recorre el perímetro y casi nunca acaba justo donde empezó, así que hay una discontinuidad visible en la esquina inicial. Si eso molesta, hay que elegir un patrón que divida exactamente el perímetro:
function guionesQueCierran(perimetro, guionDeseado) { const par = guionDeseado * 2; // guion + hueco const n = Math.max(1, Math.round(perimetro / par)); const real = perimetro / n / 2; return [real, real];}lineDashOffset: animar y revelar
lineDashOffset desplaza el punto de inicio del patrón a lo largo de la ruta. Con él se consiguen dos efectos muy útiles.
El primero es la marcha de hormigas, la selección animada de los editores gráficos:
let desfase = 0;function animarSeleccion(rect) { desfase -= 1; ctx.save(); ctx.setLineDash([6, 4]); ctx.lineDashOffset = desfase; ctx.strokeStyle = '#f9e2af'; ctx.lineWidth = 1.5; ctx.strokeRect(rect.x + 0.5, rect.y + 0.5, rect.w, rect.h); ctx.restore(); requestAnimationFrame(() => animarSeleccion(rect));}El segundo es más interesante: revelar un trazo progresivamente. La técnica consiste en usar un patrón cuyo guion sea la longitud total de la ruta y cuyo hueco también lo sea, y desplazar el desfase de la longitud total a cero.
/** Dibuja la ruta como si se estuviera trazando, de 0 a 1. */function trazarProgresivo(ctx, longitud, progreso) { ctx.setLineDash([longitud, longitud]); ctx.lineDashOffset = longitud * (1 - progreso); ctx.stroke(); ctx.setLineDash([]);}El problema es conocer la longitud de la ruta, y el canvas no la expone. Para rutas rectas se calcula sumando distancias; para curvas hay que aproximar, o recurrir a un truco: crear un elemento <path> de SVG con la misma geometría y preguntarle su longitud.
function longitudDeD(d) { const p = document.createElementNS('http://www.w3.org/2000/svg', 'path'); p.setAttribute('d', d); return p.getTotalLength();}const d = 'M20 120 C 80 20, 180 20, 240 120';const largo = longitudDeD(d);const ruta = new Path2D(d);let t = 0;function marco() { ctx.clearRect(0, 0, 300, 160); ctx.strokeStyle = '#cba6f7'; ctx.lineWidth = 4; ctx.setLineDash([largo, largo]); ctx.lineDashOffset = largo * (1 - t); ctx.stroke(ruta); ctx.setLineDash([]); t = Math.min(1, t + 0.01); if (t < 1) requestAnimationFrame(marco);}marco();Ese getTotalLength sobre un elemento SVG desconectado del documento funciona y es la forma estándar de medir longitudes de ruta en la web. El elemento ni siquiera necesita estar insertado.
Interacciones que hay que recordar
Con lineCap. Los remates se aplican a cada guion, no solo a los extremos de la línea. Con round y un patrón de [4, 4] y grosor 8, cada guion crece cuatro píxeles por lado y el hueco desaparece: la línea se ve continua. Si usas guiones, butt suele ser lo correcto, y si quieres puntos redondos usa un guion de longitud cero con round.
// Linea de puntos redondos perfectosctx.lineCap = 'round';ctx.lineWidth = 6;ctx.setLineDash([0, 14]); // guion de longitud cero + huecoctx.stroke();Con la escala. El patrón está en unidades del espacio actual, así que un contexto escalado escala también los guiones. Es coherente, y significa que al hacer zoom los guiones crecen. Si quieres guiones de tamaño constante en pantalla independientemente del zoom, divide el patrón por la escala.
Con el estado. setLineDash y lineDashOffset forman parte del estado y los guarda save. Como el patrón afecta a todo lo que se trace después, la disciplina de restaurarlo es obligatoria: una función que deja guiones activos convierte todos los trazos posteriores en discontinuos, y ese bug se diagnostica fatal porque el síntoma aparece lejos de la causa.