Документ, фигура и координаты редактора
В этой серии мы создадим редактор «Схема». В нём можно расположить прямоугольники и эллипсы, подписать их, изменить цвет, сохранить документ и получить изображение. Такой инструмент подходит для небольшой иллюстрации учебного процесса: например, показать редактор материалов и библиотеку сайта. Сегодня определим данные, которые будут описывать эту иллюстрацию. После урока у вас будет документ, пригодный для рисования, сохранения и последующего редактирования.
Понадобится понимание объектов, массивов и модулей JavaScript. Итоговые исходники находятся в архиве серии. Это один законченный снимок проекта; фрагменты уроков объясняют его устройство. Пример подготовлен локально и ещё не запускался. Числа и результаты ниже служат ориентиром для самостоятельной проверки.
Что именно хранит схема
Начнём с обычной страницы размером 1000 на 700 условных единиц. Единица документа пока не равна пикселю экрана. Браузер может показать страницу шириной 500 пикселей, а экспортировать изображение шириной 2000. Положение фигур при этом останется прежним. Если сразу сохранить координаты в пикселях текущего окна, изменение размера браузера начнёт менять смысл документа.
Документ содержит номер формата schema, размеры страницы и массив shapes. Порядок массива одновременно задаёт порядок рисования: более поздняя фигура находится поверх более ранней. Для каждого элемента сохраняем идентификатор, тип, положение, размеры, цвет и подпись. Выделение и масштаб просмотра появятся позднее, но записывать их в этот объект мы не будем: они относятся к работе редактора, а не к самой иллюстрации.
Сокращённый пример документа выглядит так:
const document = {
schema: 1,
width: 1000,
height: 700,
shapes: [{
id: 'editor', type: 'rect',
x: 100, y: 120, width: 240, height: 120,
fill: '#8756d8', label: 'Редактор'
}]
};
Это данные для объяснения, а не замена всего model.mjs. В полном файле функция sampleDocument() добавляет ещё зелёный эллипс «Библиотека». У прямоугольника x и y обозначают верхний левый угол. Для эллипса мы используем такие же поля: они задают угол ограничивающего прямоугольника. Благодаря этому панель свойств не должна по-разному вычислять положение двух типов фигур.
Центр эллипса получается как x + width / 2, y + height / 2, а радиусы — как половины размеров. При x = 420, width = 240 центр по горизонтали равен 540. Это простая договорённость, но она будет участвовать в рисовании, определении попадания указателя и экспорте. Если хотя бы один модуль начнёт считать x центром, изображение и область выбора разойдутся.
Допустимые значения
Редактор принимает страницу от 100 до 2000 единиц по каждой стороне и не более 200 фигур. Эти пределы выбраны для учебной задачи. Они делают размер работы обозримым и позволяют хранить небольшие снимки истории. Такие числа не являются доказательством быстродействия на конкретном телефоне: производительность нужно будет измерять отдельно.
Фигура должна полностью помещаться на странице. Поэтому для горизонтального положения действует условие 0 ≤ x ≤ document.width - shape.width. Если ширина страницы равна 1000, а ширина прямоугольника 240, максимально допустимое значение x равно 760. Проверка просто на x ≤ 1000 разрешила бы вынести большую часть фигуры за правый край.
Все координаты должны быть конечными числами. Значения NaN, Infinity, строки и отсутствующие свойства не подходят. Дробные координаты допускаются: движение указателя и масштабирование могут давать их естественным образом. Тип ограничен двумя значениями, цвет — шестизначной HEX-записью, подпись — 120 символами. Это сохраняет небольшой и понятный набор возможностей.
| Поле | Значение для нашего примера | Зачем проверять |
|---|---|---|
schema |
1 |
Отличить известный формат от будущего |
id |
editor |
Связать выбор, команды и фигуру |
type |
rect или ellipse |
Выбрать алгоритм геометрии |
fill |
#8756d8 |
Не принимать произвольные стили |
label |
Обычная строка | Ограничить объём и способ вывода |
Идентификаторы должны быть уникальными. Две фигуры с одинаковым id делают команду «передвинуть editor» неоднозначной. Проверяем повтор через Set при чтении всего массива. При добавлении в интерфейсе используем crypto.randomUUID(), но импорт всё равно проходит общую проверку: имя функции генерации не делает внешний JSON доверенным.
Проверка возвращает новый объект
Функция validateDocument() из model.mjs проверяет вход и собирает результат из разрешённых полей. Она не возвращает исходный объект целиком. Это означает, что случайные поля вроде onclick, неизвестного URL изображения или сохранённой камеры не перейдут в рабочую модель. Мы не превращаем JSON в универсальный язык SVG.
return {
schema: 1,
width: value.width,
height: value.height,
shapes
};
Этот фрагмент показывает окончание проверки. До него функция уже проверила каждую фигуру и заново создала элементы массива. Такой подход полезен на границе приложения: после чтения файла, восстановления локального состояния и изменения свойств мы получаем одинаковую модель. Компонент рисования может работать с ней, не повторяя весь разбор JSON.
Проверка структуры не решает каждую задачу редактора. Например, она разрешает длинную подпись, которая визуально выйдет за узкую фигуру. В нашей первой версии текст не переносится автоматически. Это ограничение интерфейса следует описать отдельно; добавление переноса позднее потребует одинаковых правил для SVG и PNG.
Стабильный идентификатор также позволяет менять подпись без потери связи с выделением. Имя «Редактор» может стать «Статьи», но объект останется editor. Если использовать подпись как ключ, одинаковые названия или исправление текста нарушат команды. По той же причине индекс массива не подходит в качестве постоянного имени: перестановка слоёв меняет индекс, хотя фигура остаётся той же.
Номер формата не следует увеличивать при каждом косметическом изменении интерфейса. Он относится к устройству сохранённых данных. Добавление новой обязательной геометрии потребует решения о миграции старых файлов; новый цвет кнопки — нет. Такая дисциплина позволит отдельно развивать оболочку и переносимый документ.
Как разобрать результат
Просмотрите sampleDocument() и выпишите правые края обеих фигур. Для прямоугольника получится 340, для эллипса — 660. Оба значения меньше ширины страницы 1000. Затем мысленно замените x прямоугольника на 900: проверка должна отклонить документ, потому что 900 + 240 выходит за край. Повторите разбор с двумя одинаковыми идентификаторами и неизвестным типом triangle.
При самостоятельном запуске ожидается отказ для этих вариантов и принятие исходного документа. Важно проверять именно границы модели, а не только красивую картинку. Редактор с неверными данными может выглядеть исправно до первой отмены или загрузки файла. Теперь у нас есть единый договор для всех следующих модулей. В следующем уроке превратим его в SVG, сохранив данные источником изображения.