Path2D desde una cadena de datos SVG
Usar el mini-lenguaje de rutas de SVG dentro del canvas, y aprovechar el puente entre ambos mundos para reutilizar iconos, medir longitudes y convertir formatos.
El constructor de Path2D acepta una cadena con el atributo d de un trazado SVG y la convierte en geometría de canvas. Ese puente parece un detalle menor y es una de las funcionalidades más útiles de toda la API: permite usar cualquier icono, mapa o forma vectorial que exista como SVG dentro de un canvas, sin conversión y sin dependencias.
- Construir un
Path2Da partir de una cadena de datos SVG. - Reconocer los comandos del mini-lenguaje y su equivalencia con la API del canvas.
- Manejar los errores de análisis, que son silenciosos.
- Combinar
Path2Dcon la API de SVG para medir longitudes y puntos.
El constructor con cadena
const corazon = new Path2D(
'M12 21s-8.5-5.4-8.5-11A4.5 4.5 0 0 1 12 7a4.5 4.5 0 0 1 8.5 3c0 5.6-8.5 11-8.5 11z'
);
ctx.fillStyle = '#f38ba8';
ctx.fill(corazon);
Esa cadena es exactamente el atributo d de un <path> de SVG, sin modificación alguna. Cualquier icono de cualquier colección se pega tal cual.
La equivalencia entre los comandos y la API del canvas es directa:
| Comando SVG | Equivalente en canvas |
|---|---|
M x y |
moveTo(x, y) |
L x y |
lineTo(x, y) |
H x |
lineTo(x, yActual) |
V y |
lineTo(xActual, y) |
C x1 y1 x2 y2 x y |
bezierCurveTo(...) |
Q x1 y1 x y |
quadraticCurveTo(...) |
S, T |
Como C y Q con el control reflejado |
A rx ry rot arcoGrande barrido x y |
Arco elíptico; no hay equivalente directo |
Z |
closePath() |
Las mayúsculas usan coordenadas absolutas y las minúsculas, relativas al punto actual. Los comandos S y T reflejan automáticamente el control anterior, que en la API del canvas hay que calcular a mano. Y el comando A, el arco elíptico, no tiene equivalente directo en el canvas: ellipse recibe centro y ángulos, mientras que A recibe el punto final y unas banderas. Convertir entre ambas parametrizaciones es un cálculo largo y desagradable, y el constructor de Path2D lo hace por ti. Solo por eso ya merece la pena.
Los errores son silenciosos
Si la cadena tiene un error de sintaxis, el constructor no lanza: procesa lo que puede y descarta el resto, o devuelve una ruta vacía. La consecuencia es una forma que no aparece o que aparece a medias, sin ningún mensaje.
const mala = new Path2D('M10 10 L 50 zz 90');
ctx.fill(mala); // dibuja algo raro o nada, sin avisar
Como no hay forma de preguntarle a un Path2D si está vacío, la validación hay que hacerla antes. La forma más fiable usa el propio SVG, que sí valida:
function rutaValida(d) {
const p = document.createElementNS('http://www.w3.org/2000/svg', 'path');
p.setAttribute('d', d);
try {
return p.getTotalLength() > 0;
} catch {
return false;
}
}
Un elemento SVG desconectado del documento funciona perfectamente para esto y no cuesta nada.
Escalar y colocar
Un icono SVG viene diseñado dentro de un viewBox, normalmente de 24 por 24. Para dibujarlo a otro tamaño y en otra posición, no se modifica la cadena: se transforma el contexto.
function icono(ctx, ruta, x, y, tamano, viewBox = 24, color = '#cdd6f4') {
const s = tamano / viewBox;
ctx.save();
ctx.translate(x, y);
ctx.scale(s, s);
ctx.fillStyle = color;
ctx.fill(ruta);
ctx.restore();
}
Si prefieres que el icono quede centrado en su punto de anclaje —que suele ser lo más cómodo— la traslación incluye media caja:
ctx.translate(x - tamano / 2, y - tamano / 2);
Y si vas a usar siempre el mismo tamaño, es más eficiente pre-transformar la ruta una vez con addPath y una matriz, en lugar de transformar el contexto en cada dibujo:
function escalarRuta(ruta, factor) {
const salida = new Path2D();
salida.addPath(ruta, new DOMMatrix().scale(factor));
return salida;
}
El puente completo con SVG
La combinación de Path2D con los elementos SVG desconectados da acceso a operaciones geométricas que el canvas no tiene.
Longitud total de una ruta, imprescindible para el trazado progresivo con setLineDash:
function longitud(d) {
const p = document.createElementNS('http://www.w3.org/2000/svg', 'path');
p.setAttribute('d', d);
return p.getTotalLength();
}
Un punto a una distancia dada a lo largo de la ruta, que es la base de colocar objetos sobre un camino y de la animación por trayectoria:
function puntoEn(d, fraccion) {
const p = document.createElementNS('http://www.w3.org/2000/svg', 'path');
p.setAttribute('d', d);
const largo = p.getTotalLength();
const punto = p.getPointAtLength(largo * fraccion);
// Aproximar la tangente con un punto muy cercano
const siguiente = p.getPointAtLength(Math.min(largo, largo * fraccion + 0.5));
return { x: punto.x, y: punto.y,
angulo: Math.atan2(siguiente.y - punto.y, siguiente.x - punto.x) };
}
Con esas dos funciones y un Path2D se construye la animación de un objeto que recorre un camino, orientado según la tangente:
const d = 'M20 200 C 120 20, 280 380, 380 200';
const camino = new Path2D(d);
const largo = longitud(d);
let t = 0;
function marco() {
ctx.clearRect(0, 0, 400, 400);
ctx.strokeStyle = '#45475a'; ctx.lineWidth = 2;
ctx.stroke(camino);
const p = puntoEn(d, t);
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.angulo);
ctx.fillStyle = '#a6e3a1';
ctx.beginPath();
ctx.moveTo(14, 0); ctx.lineTo(-10, 8); ctx.lineTo(-10, -8);
ctx.closePath(); ctx.fill();
ctx.restore();
t = (t + 0.004) % 1;
requestAnimationFrame(marco);
}
marco();
Ese puntoEn crea un elemento SVG en cada llamada, lo cual es inaceptable dentro de un bucle. La versión de producción crea el elemento una vez y lo reutiliza.
Convertir una cadena d en geometría exige un análisis sintáctico completo: tokenizar números en notación posiblemente científica, resolver comandos relativos acumulando el punto actual, reflejar controles en los comandos abreviados y —lo más caro con diferencia— convertir cada arco elíptico de la parametrización de punto final a la de centro y ángulos, que implica varias raíces cuadradas y funciones trigonométricas por arco. Para un icono de veinte comandos eso es despreciable. Para el trazado de un país en un mapa, con veinte mil puntos, son decenas de milisegundos por ruta. Y aquí está el error que hunde aplicaciones de mapas enteras: construir el Path2D dentro del bucle de dibujo. El código parece razonable porque la cadena está ahí en los datos, y el resultado es que en cada fotograma se reanalizan megabytes de texto. El síntoma es inconfundible: el mapa va bien con cinco regiones y se congela con doscientas, y el perfil muestra todo el tiempo dentro del constructor. La regla es tajante: new Path2D(cadena) es una operación de carga, no de dibujo. Va donde va el análisis del JSON, no donde va el fill. Y hay dos consecuencias útiles de la misma observación. La primera: si tus rutas vienen del servidor, cachea los objetos Path2D por identificador y no por cadena, porque comparar cadenas largas también cuesta. La segunda: si la geometría es enorme y la vas a dibujar a un zoom bajo, simplifícala antes de construir la ruta. Un algoritmo de Douglas-Peucker con una tolerancia de medio píxel puede reducir un trazado de veinte mil puntos a trescientos sin ninguna diferencia visible, y eso reduce tanto el análisis como el rasterizado.
Cargar un SVG y extraer sus rutas
Para usar un fichero SVG completo dentro de un canvas, el camino más directo es analizarlo con el propio navegador y extraer los atributos d:
async function rutasDeSVG(url) {
const texto = await (await fetch(url)).text();
const doc = new DOMParser().parseFromString(texto, 'image/svg+xml');
const raiz = doc.documentElement;
const vb = (raiz.getAttribute('viewBox') || '0 0 24 24').split(/\s+/).map(Number);
const rutas = [...doc.querySelectorAll('path')].map(p => ({
ruta: new Path2D(p.getAttribute('d')),
relleno: p.getAttribute('fill') || 'currentColor',
trazo: p.getAttribute('stroke'),
grosor: parseFloat(p.getAttribute('stroke-width') || '0'),
}));
return { rutas, viewBox: { x: vb[0], y: vb[1], ancho: vb[2], alto: vb[3] } };
}
Esa función cubre los SVG de icono, que son la inmensa mayoría de los casos. No cubre gradientes, máscaras, filtros, grupos con transformaciones, ni los elementos de forma que no son path. Para eso hay que recorrer el árbol completo y traducir cada elemento, que es un trabajo considerable. Cuando el SVG es complejo, la respuesta correcta es dibujarlo con drawImage desde una <img> cuyo src sea el SVG, y aceptar que se rasteriza como una imagen.