Saltar al contenido

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 con cadena

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

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

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

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

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

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

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

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

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

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:

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