Реализация и спецификация SVG-редактора, работающего только в браузере
Ключевые моменты статьи
- Пытался вместе с 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 и построить функциональный редактор, который полностью работает в браузере.