Перейти к содержанию

SVG из данных, без HTML-строк

В предыдущем уроке мы определили документ «Схемы». Теперь построим изображение из его данных. Основное полотно редактора будет SVG: прямоугольники, эллипсы и подписи остаются отдельными элементами, а масштабирование выполняет браузер. Canvas понадобится позднее для растрового экспорта. К концу этого урока одна модель будет описывать две видимые фигуры без ручного хранения их разметки.

Код находится в view.mjs архива серии. Пример ещё не запускался; внешний вид и поведение ниже описаны как ожидаемые. Для продолжения нужны модель первого урока и знания создания DOM-элементов. Сейчас рассматриваем рисование, а управление указателем добавим отдельно.

Корневой элемент и система координат

В index.html располагается постоянный элемент <svg id="scene">. Его размер на странице определяет CSS, а viewBox задаёт видимую область координат документа. В начальном состоянии значение равно 0 0 1000 700. При уменьшении ширины контейнера браузер масштабирует содержимое, сохраняя координаты фигур в модели.

Согласно описанию viewBox в MDN, атрибут состоит из положения и размеров прямоугольной области. Поэтому четыре числа не следует читать как экранные отступы. Первые два — координаты начала области, следующие два — её ширина и высота. Позднее, изменяя эту область, мы приблизим схему, не увеличивая сами фигуры.

SVG-элементы создаются в своём пространстве имён. Обычный document.createElement('rect') не выражает тот же договор. В файле view.mjs есть небольшая функция:

const NS = 'http://www.w3.org/2000/svg';

export function svgNode(tag, attrs = {}) {
  const node = document.createElementNS(NS, tag);
  for (const [key, value] of Object.entries(attrs)) {
    node.setAttribute(key, String(value));
  }
  return node;
}

Это полная вспомогательная функция, но не весь модуль. Имена тегов и атрибутов задаёт наш код. Внешний документ не содержит произвольного тега, набора стилей или обработчика событий. Такая граница упрощает чтение: функция создаёт разрешённую геометрию, а не исполняет присланную разметку.

Одна фигура — одна группа

Для каждой записи создаём группу g. Внутрь помещаем геометрический элемент и текст. Прямоугольнику передаём x, y, width, height. Эллипсу вычисляем cx, cy, rx, ry из тех же полей модели. Порядок групп совпадает с порядком массива shapes, поэтому последний элемент оказывается выше предыдущих.

Группа удобна для совместного представления фигуры и её подписи. При этом мы не сохраняем ссылку на неё в документе. DOM — результат рисования, который можно создать заново. Если начать хранить одновременно объект фигуры и изменяемую SVG-группу, придётся поддерживать два состояния: новое значение координат легко попадёт только в одно из них.

const text = svgNode('text', {
  x: shape.x + shape.width / 2,
  y: shape.y + shape.height / 2,
  'text-anchor': 'middle',
  'dominant-baseline': 'central'
});
text.textContent = shape.label;

Здесь показан фрагмент создания подписи; полный модуль также задаёт цвет, размер и семейство шрифта. textContent выводит содержимое как текст. Если подпись содержит <script> или угловые скобки из примера кода, они не должны превращаться в дочернюю разметку. Ограничение длины из первого урока продолжает действовать, но само по себе оно не заменило бы этот способ вывода.

Горизонтальная точка подписи находится в центре фигуры, а text-anchor задаёт центрирование относительно неё. Вертикальное положение зависит от метрик шрифта и поддержки базовых линий. Учебный пример использует системный sans-serif, поэтому точное начертание может отличаться между устройствами. Для одинаковой фирменной типографики потребуются собственный шрифт и отдельное решение для экспорта.

Обновление сцены

Функция renderSvg(svg, doc, selected, camera) строит набор узлов и вызывает svg.replaceChildren(...nodes). Начинаем с title и белой страницы, затем рисуем фигуры. Сам корневой SVG остаётся тем же объектом. Это важно: обработчики ввода и захват указателя позднее будут прикреплены к корню, а не к временным элементам фигур.

Мы сознательно выбрали простое полное обновление дочерних элементов. Для небольшого документа оно легче для разбора, чем сопоставление каждой старой группы с новой. Но предел в 200 фигур не доказывает отсутствие задержек. Частые перерисовки и создание текста нужно будет оценивать на реальных устройствах; при росте проекта можно перейти к обновлению конкретных атрибутов.

Выделенная фигура получает тёмную обводку толщиной четыре единицы, остальные — белую толщиной одну. Аргумент selected передаётся отдельно и не меняет массив фигур. Если экспортировать документ, задав selected = null, получим обычную иллюстрацию без следов текущей работы пользователя. Этот небольшой выбор сразу пригодится при скачивании SVG.

Источник Что меняется в изображении Что остаётся в файле документа
doc.shapes Геометрия и подписи Сами фигуры
selected Обводка выбранной фигуры Ничего
camera Видимая область Ничего

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

Не стоит читать готовое дерево SVG обратно для автосохранения. В нём присутствуют рабочая обводка, заголовок и фон, которых нет в модели. Кроме того, DOM содержит строковые представления чисел. Сохранение модели сохраняет смысл документа, а сериализация SVG нужна для другого результата — изображения. Это различие позволяет менять рендерер, не ломая старые JSON-файлы.

От модели к наблюдаемому результату

Для самостоятельного разбора сопоставьте поля второй фигуры с созданным эллипсом. При ширине 240 ожидается rx = 120, при высоте 120 — ry = 60. Центр должен находиться в точке 540, 180. Затем измените порядок массива: изображение должно использовать новый порядок без отдельного свойства z-index у каждого элемента.

Ещё один полезный случай — пустой массив. Рендерер должен показать белую страницу и заголовок с нулём фигур, а не обращаться к несуществующему первому объекту. Попробуйте подпись с символами < и >. Ожидается обычный текст, а не появление дополнительных DOM-элементов. Эти сценарии проверяют договор рисования лучше, чем сравнение одного удачного снимка.

Полотно пока описано как изображение с текстовым названием. Это не даёт пользователю клавиатуры возможности перемещать фигуру. В шестом и десятом уроках сделаем отдельные HTML-элементы управления, связанные с той же моделью. Так визуальное представление и доступные действия будут развиваться вместе. Следующий шаг — научиться переводить координаты указателя в координаты документа и выбирать верхнюю фигуру под ним.

Оглавление серии