Puntos clave de este artículo

  • Intenté construir el editor SVG (/es/tools/svgedit/) codo a codo con ChatGPT —en realidad dando instrucciones como gestor— y este texto resume todos los tropiezos que encontré.
  • Nunca lo habría logrado en solitario. La capacidad de programación de la IA generativa sigue siendo asombrosa.
  • Pero tampoco funcionó delegar todo. Hizo falta coordinar el diseño global, corregir requisitos mal entendidos y marcar el rumbo cuando surgían incidentes: la función de un playing manager en toda regla.
  • En ese sentido, la experiencia deja pistas de cómo sobrevivir a la era de la IA.

Diseñar e implementar un editor SVG que se completa solo en el navegador

¿Qué es SVG?

SVG (Scalable Vector Graphics) es un formato de imagen vectorial basado en XML estandarizado por el W3C. A diferencia de las imágenes rasterizadas (como PNG o JPEG), puede ampliarse o reducirse sin perder calidad. Además, se puede incrustar directamente en un documento HTML como parte del DOM, por lo que permite manipulación dinámica mediante JavaScript y CSS. La especificación sigue la recomendación SVG del W3C e incluye formas, texto, trazados, degradados, filtros y más recursos expresivos.

SVG se utiliza ampliamente en casos como los siguientes:

  • Mostrar iconos o diagramas en páginas web.
  • Generar diagramas y gráficos dinámicos.
  • Crear aplicaciones de dibujo con interacción de usuario.
  • Preparar recursos para impresión o diseño de interfaces.

Descripción general del editor

El editor es una herramienta completamente del lado del cliente que se ejecuta en el navegador. Permite dibujar no solo rectángulos y elipses, sino también estrellas, globos de diálogo, nubes, corazones y curvas libres con un modo pluma. Las figuras pueden moverse, escalarse y rotarse; además cuenta con alineación, ajuste (snap), agrupación y una lista de capas. El resultado final puede exportarse como SVG o PNG.

A continuación explico cómo llevé la especificación de SVG a JavaScript y en qué puntos me tropecé.

Cómo reflejar la especificación en la implementación y los tropiezos

Añadir figuras y gestionar atributos

La especificación SVG define elementos y atributos específicos por tipo de figura. Los rectángulos usan <rect>, las elipses <ellipse>, los polígonos <polygon>, y cada uno requiere atributos como x, y, width, height, cx, cy, rx, ry o points configurados correctamente. Las formas complejas —estrellas, corazones— se generan con <path> combinando curvas Bézier y comandos de arco.

Ejemplo de implementación (alta de un rectángulo)

function addRectangle(x, y, width, height) {
  const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
  rect.setAttribute("x", x);
  rect.setAttribute("y", y);
  rect.setAttribute("width", width);
  rect.setAttribute("height", height);
  rect.setAttribute("fill", "transparent");
  rect.setAttribute("stroke", "#000");
  svg.appendChild(rect);
}

Rectángulos y elipses son sencillos, pero los polígonos o formas personalizadas exigen cálculos de coordenadas. Para dibujar una estrella, por ejemplo, hay que dividir el ángulo central, calcular cada vértice y volcarlo en points.

function createStar(cx, cy, spikes, outerR, innerR) {
  let points = "";
  const step = Math.PI / spikes;
  for (let i = 0; i < 2 * spikes; i++) {
    const r = i % 2 === 0 ? outerR : innerR;
    const x = cx + r * Math.cos(i * step);
    const y = cy + r * Math.sin(i * step);
    points += `${x},${y} `;
  }
  const polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
  polygon.setAttribute("points", points.trim());
  polygon.setAttribute("stroke", "#000");
  polygon.setAttribute("fill", "transparent");
  svg.appendChild(polygon);
}

Transformaciones y sistemas de coordenadas

SVG permite combinar translate, rotate y scale mediante el atributo transform. El reto fue obtener la caja delimitadora (bounding box) de la figura seleccionada teniendo en cuenta la diferencia entre coordenadas locales y globales. Utilicé getScreenCTM() para convertir todo al sistema global antes de calcular el rectángulo.

Ejemplo con la transformación aplicada

function getTransformedBBox(element) {
  const bbox = element.getBBox();
  const matrix = element.getScreenCTM();
  const points = [
    svg.createSVGPoint(), svg.createSVGPoint(),
    svg.createSVGPoint(), svg.createSVGPoint()
  ];
  points[0].x = bbox.x; points[0].y = bbox.y;
  points[1].x = bbox.x + bbox.width; points[1].y = bbox.y;
  points[2].x = bbox.x; points[2].y = bbox.y + bbox.height;
  points[3].x = bbox.x + bbox.width; points[3].y = bbox.y + bbox.height;
  return points.map(p => p.matrixTransform(matrix));
}

Con ello, incluso tras rotaciones o escalados pude dibujar correctamente el marco de selección.

Ajuste a rejilla (snap) y grilla

La rejilla se dibuja en el fondo con <pattern>. El ajuste se resuelve calculando coordenadas en JavaScript y redondeándolas al intervalo configurado.

function snapToGrid(value, gridSize) {
  return Math.round(value / gridSize) * gridSize;
}

El ajuste a otros objetos recorre centros y extremos de las figuras existentes para localizar el punto más cercano, mostrar guías y fijar el objeto. Esto elevó notablemente la precisión de la UI.

Edición de texto

Como <text> no admite edición directa, superpuse temporalmente un <input> de HTML para editar en línea.

function editTextElement(textElem) {
  const input = document.createElement("input");
  input.type = "text";
  input.value = textElem.textContent;
  document.body.appendChild(input);
  const bbox = textElem.getBoundingClientRect();
  input.style.position = "absolute";
  input.style.left = bbox.x + "px";
  input.style.top = bbox.y + "px";
  input.focus();
  input.addEventListener("blur", () => {
    textElem.textContent = input.value;
    document.body.removeChild(input);
  });
}

Trazados y curvas libres

<path> combina comandos como M, L, C o A para dibujar. Empecé con edición basada en líneas rectas, sumando coordenadas con cada clic.

let pathData = "M100,100";
function addPathPoint(x, y) {
  pathData += ` L${x},${y}`;
  path.setAttribute("d", pathData);
}

Exportación (SVG/PNG)

Al guardar elimino los elementos auxiliares y exporto. La conversión a PNG pasa por un canvas.

function exportToPNG(svgElement) {
  const serializer = new XMLSerializer();
  const source = serializer.serializeToString(svgElement);
  const img = new Image();
  const svgBlob = new Blob([source], { type: "image/svg+xml;charset=utf-8" });
  const url = URL.createObjectURL(svgBlob);
  img.onload = () => {
    const canvas = document.createElement("canvas");
    canvas.width = img.width;
    canvas.height = img.height;
    const ctx = canvas.getContext("2d");
    ctx.drawImage(img, 0, 0);
    canvas.toBlob(blob => {
      const a = document.createElement("a");
      a.href = URL.createObjectURL(blob);
      a.download = "drawing.png";
      a.click();
    });
  };
  img.src = url;
}

Soporte de desplazamiento en el lienzo

Para manejar SVG grandes, envolví el lienzo con <div style="overflow:auto"> y, cuando es necesario, muestro barras de desplazamiento.

Conclusión

La especificación SVG es poderosa, pero la implementación tropieza con obstáculos como la conversión de coordenadas o la definición de trazados. En este editor logré ordenar esos frentes y cubrir:

  • Manejo de atributos de figuras (rect, ellipse, polygon, path).
  • Uso correcto de transform y de los sistemas de coordenadas.
  • Implementación de rejilla y ajustes (snap).
  • Edición de texto en línea.
  • Edición de trazados basados en líneas.
  • Exportación a SVG y PNG.
  • Soporte para lienzos grandes desplazables.

Así pude construir un editor potente que explota al máximo las capacidades de SVG y funciona íntegramente en el navegador.