本文要点

  • 我与 ChatGPT 以分工协作的方式尝试构建 SVG 编辑器 /tools/svgedit/,过程中屡屡受阻,本文汇总了那些难点。
  • 若是单打独斗我绝对办不到,生成式 AI 的编码能力确实惊人。
  • 但也无法完全托付给 AI:仍需把握整体架构,修正错误需求,指明故障排除方向——十足的“边做边带队”角色。
  • 从这个角度看,这段经历也提示了在 AI 时代生存的线索。

仅靠浏览器完成的SVG编辑器的实现与规格解析

什么是 SVG

SVG(Scalable Vector Graphics,可缩放矢量图形)是基于 XML 的矢量图格式,由 W3C 标准化。它与光栅图像(如 PNG、JPEG)不同,缩放时不会损失画质。由于 SVG 可作为 DOM 的一部分直接嵌入 HTML 文档,因此能够通过 JavaScript 与 CSS 进行动态操作和样式变换。该格式遵循 W3C SVG 规范,具备图形、文字、路径、渐变、滤镜等丰富的表现力。

SVG 广泛用于以下场景:

  • 网页上的图标或图表展示
  • 动态生成图表与流程图
  • 带有用户交互的绘图应用
  • 制作印刷品与 UI 设计素材

本次 SVG 编辑器概览

本编辑器完全在浏览器端运行。用户不仅能绘制矩形、椭圆等基本图形,还可以生成星形、对话框、云朵、爱心,以及通过钢笔模式绘制自由曲线。图形支持移动、缩放、旋转,并具备对齐、吸附、分组与图层列表等功能。最终作品可导出为 SVG 或 PNG。

下文将具体说明如何把 SVG 规范落实到 JavaScript 实现中。

在实现过程中映射规范与踩坑记录

添加图形与属性处理

SVG 规范为每种图形定义了专用元素与属性。矩形使用 <rect>,椭圆用 <ellipse>,多边形用 <polygon>;需要正确设置 xywidthheightcxcyrxrypoints 等属性。星形、爱心等复杂形状则通过 <path> 元素组合贝塞尔曲线与弧指令生成。

实现示例(添加矩形)

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

矩形和椭圆较为简单,但多边形与自定义图形需要事先计算坐标。以星形为例,需要将中心角均分,算出各个顶点并写入 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);
}

变换与坐标系处理

在 SVG 中,可以利用 transform 属性组合 translaterotatescale 等操作来控制图形。难点在于获取选中图形的包围盒时,需要消除局部坐标与全局坐标之间的差异。我借助 getScreenCTM() 转换到全局坐标系,再据此计算矩形范围。

实现示例(应用坐标变换)

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

这样一来,即便已经旋转或缩放,选框仍能准确绘制。

吸附功能与网格

网格背景通过 SVG 的 <pattern> 元素绘制。吸附逻辑在 JavaScript 中完成,把坐标按指定间隔取整。

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

对象吸附则遍历其他图形的中心点和端点,寻找最近的候选,并显示辅助线以完成吸附,大幅提升了操作精度。

文本编辑

SVG 的 <text> 元素无法直接编辑,因此我临时叠加一个 HTML <input> 进行行内编辑。

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

路径与自由曲线

SVG 的 <path> 元素通过组合 M、L、C、A 等指令来绘制路径。我先从折线编辑入手,每次点击都会把坐标追加到路径中。

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

输出(SVG/PNG 导出)

保存时会先移除辅助元素再导出。转换为 PNG 则借助中间的 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;
}

画布滚动的支持

处理大尺寸 SVG 时,我会用 <div style="overflow:auto"> 包裹画布,以便在需要时出现滚动条。

总结

SVG 的规范十分强大,但在实现中必须跨越坐标变换与路径定义等高门槛。本编辑器梳理了这些问题,实现了以下功能:

  • 操作图形元素的属性(rectellipsepolygonpath
  • 正确处理 transform 与坐标系
  • 实现网格与吸附
  • 支持文本的行内编辑
  • 以折线为基础的路径编辑
  • 导出 PNG 与 SVG
  • 面向大画布的滚动支持

充分发挥了 SVG 的表现力,从而构建出一个功能丰富、完全在浏览器端运行的编辑器。