Ключевые моменты статьи

  • Пытался вместе с ChatGPT, выступая в роли постановщика задач, создать SVG-редактор /tools/svgedit/, но постоянно наталкивался на препятствия — здесь собраны главные сложности.
  • В одиночку я бы точно не справился: кодерские способности генеративного ИИ по-настоящему впечатляют.
  • ИИ тоже не смог сделать всё сам: приходилось держать общую архитектуру, исправлять неверные требования и задавать курс при сбоях — то есть играть роль играющего менеджера.
  • Этот опыт подсказал, как оставаться на плаву в эпоху ИИ.

Реализация и спецификация SVG-редактора, работающего только в браузере

Что такое SVG

SVG (Scalable Vector Graphics) — векторный графический формат на основе XML, стандартизованный W3C. В отличие от растровых изображений (PNG или JPEG), он не теряет качества при масштабировании. Кроме того, SVG можно встроить напрямую в HTML-документ как часть DOM, поэтому через JavaScript и CSS доступны динамические операции и изменение стилей. Формат соответствует спецификации W3C SVG и предоставляет широкий набор выразительных средств: фигуры, текст, пути, градиенты, фильтры и многое другое.

SVG широко используется в следующих случаях:

  • Отображение иконок или диаграмм на веб-страницах
  • Динамическая генерация диаграмм и графиков
  • Приложения для рисования с пользовательским взаимодействием
  • Подготовка материалов для печати и дизайна интерфейсов

Обзор разработанного SVG-редактора

Этот редактор полностью работает на стороне клиента в браузере. Пользователь может создавать разнообразные фигуры — от прямоугольников и эллипсов до звёзд, выносок, облаков, сердечек и свободных кривых в режиме пера. Каждую фигуру можно перемещать, масштабировать и вращать; предусмотрены функции выравнивания, привязки, группировки и список слоёв. Готовые изображения можно экспортировать в форматах SVG и PNG.

Далее подробно объясняется, как спецификацию SVG удалось воплотить в JavaScript.

Как спецификация легла в реализацию и где возникли трудности

Добавление фигур и работа с атрибутами

В спецификации SVG для каждой фигуры определены свои элементы и атрибуты. Прямоугольники используют <rect>, эллипсы — <ellipse>, многоугольники — <polygon>, и в каждом случае нужно корректно задавать атрибуты вроде x, y, width, height, cx, cy, rx, ry или points. Сложные фигуры вроде звёзд и сердечек генерируются через элемент <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 позволяет комбинировать translate, rotate, scale и другие операции для управления фигурами. Сложность заключалась в том, чтобы при получении ограничивающего прямоугольника выбранного объекта учесть разницу между локальной и глобальной системами координат. Для этого я использовал 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));
}

Так удаётся правильно рисовать рамку выделения даже после поворотов и масштабирования.

Привязка и сетка

Сетку я рисую с помощью элемента <pattern> на фоне SVG. Логику привязки реализовал в JavaScript, округляя координаты с нужным шагом.

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

Привязка к объектам реализована путём обхода центров и крайних точек других фигур, поиска ближайшего кандидата и отображения вспомогательных линий. Это заметно улучшило точность интерфейса.

Редактирование текста

Элемент <text> в SVG нельзя редактировать напрямую, поэтому для инлайн-редактирования я временно накладываю 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);
  });
}

Пути и свободные кривые

Элемент <path> в SVG сочетает команды 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 обладает мощной спецификацией, но на практике приходится преодолевать трудности с координатами и определением путей. В этом редакторе удалось систематизировать их и реализовать следующее:

  • Управление атрибутами фигур (rect, ellipse, polygon, path)
  • Корректная работа с transform и системами координат
  • Привязка к сетке
  • Инлайн-редактирование текста
  • Редактирование путей на основе ломаных
  • Экспорт в PNG и SVG
  • Поддержка больших полотен с прокруткой

Удалось извлечь максимум из выразительности SVG и построить функциональный редактор, который полностью работает в браузере.