Principais pontos deste artigo

  • Tentei construir o editor SVG (/pt/tools/svgedit/) em dupla com o ChatGPT — atuando muito mais como gestor do que como desenvolvedor — e reuni aqui todos os tropeços do caminho.
  • Sozinho eu não teria conseguido concluir o projeto. A capacidade de codificação da IA generativa continua impressionante.
  • Ao mesmo tempo, “deixar tudo com a IA” não funcionou. Foi preciso coordenar o desenho geral, corrigir requisitos mal entendidos e indicar o rumo quando surgiam incidentes, exatamente como um playing manager.
  • Nesse sentido, a experiência trouxe pistas sobre o que é preciso para sobreviver na era da IA.

Projetar e implementar um editor SVG que roda apenas no navegador

O que é SVG?

SVG (Scalable Vector Graphics) é um formato de imagem vetorial baseado em XML padronizado pelo W3C. Diferentemente de imagens rasterizadas (PNG ou JPEG), pode ser ampliado ou reduzido sem perda de qualidade. Como pode ser incorporado diretamente a um documento HTML como parte do DOM, é possível manipulá-lo dinamicamente com JavaScript e CSS. A especificação segue a recomendação SVG do W3C e abrange formas, textos, caminhos, gradientes, filtros e muito mais.

SVG é amplamente usado em situações como:

  • Exibir ícones e diagramas em páginas web.
  • Gerar diagramas ou gráficos dinâmicos.
  • Criar aplicativos de desenho com interação do usuário.
  • Produzir materiais para impressão ou design de interfaces.

Visão geral do editor

O editor apresentado aqui é uma ferramenta totalmente client-side que roda no navegador. O usuário consegue desenhar não só retângulos e elipses, mas também estrelas, balões de diálogo, nuvens, corações e curvas livres com um modo caneta. As formas podem ser movidas, redimensionadas e rotacionadas; a interface inclui alinhamento, snap, agrupamento e lista de camadas. O trabalho final pode ser exportado em SVG ou PNG.

A seguir explico como traduzi a especificação SVG para JavaScript e onde tropecei ao longo do processo.

Como refletimos a especificação e onde tropeçamos

Adição de formas e tratamento de atributos

A especificação do SVG define elementos e atributos específicos para cada tipo de forma. Retângulos usam <rect>, elipses usam <ellipse>, polígonos usam <polygon>, e cada um exige atributos como x, y, width, height, cx, cy, rx, ry ou points configurados corretamente. Formas complexas — como estrelas ou corações — são geradas com <path> combinando curvas Bézier e comandos de arco.

Exemplo de implementação (adição de um retâ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);
}

Retângulos e elipses são diretos, mas polígonos e formas personalizadas exigem cálculos de coordenadas. Para desenhar uma estrela, por exemplo, dividimos o ângulo central igualmente e calculamos os vértices antes de gravá-los no atributo 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);
}

Transformações e sistemas de coordenadas

O SVG permite combinar translate, rotate e scale por meio do atributo transform. O desafio estava em obter a bounding box do objeto selecionado levando em conta a diferença entre coordenadas locais e globais. A solução foi usar getScreenCTM() para converter tudo ao sistema global antes de calcular o retângulo.

Exemplo com a transformação 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));
}

Assim o quadro de seleção permanece correto mesmo após rotações ou escalas.

Snap e grade

A grade é desenhada em segundo plano com <pattern>. O ajuste (snap) arredonda as coordenadas em JavaScript com um helper simples.

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

Para o snap entre objetos, o editor percorre os centros e extremidades das outras formas, encontra o candidato mais próximo e exibe linhas-guia para “magnetizar” o elemento selecionado. Isso elevou bastante a precisão da interface.

Edição de texto

Como <text> não permite edição direta, sobrepomos temporariamente um <input> HTML para editar inline.

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);
  });
}

Caminhos e curvas livres

O elemento <path> combina comandos como M, L, C e A. Começamos com edição baseada em segmentos retos, adicionando uma coordenada a cada clique.

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

Exportação (SVG/PNG)

Na hora de salvar, removemos elementos auxiliares antes de exportar. A conversão para PNG passa por um 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;
}

Lidar com pranchetas roláveis

Para SVGs grandes, bastou envolver o elemento em um <div style="overflow:auto"> e exibir barras de rolagem quando necessário.

Encerrando

A especificação SVG é poderosa, mas implementá-la exige lidar com transformações de coordenadas, definição de caminhos e muito mais. Este editor organiza essas questões e oferece:

  • Manipulação de atributos para retângulos, elipses, polígonos e paths.
  • Uso correto de transform e dos sistemas de coordenadas.
  • Suporte a grade e snap.
  • Edição de texto inline.
  • Edição de paths (por enquanto, baseada em segmentos retos).
  • Exportação para PNG e SVG.
  • Suporte a pranchetas grandes com rolagem.

Aproveitar toda a expressividade do SVG tornou possível construir um editor robusto que roda inteiramente no navegador.