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

Масштаб и перемещение камеры

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

Функция zoomCamera() находится в view.mjs, а кнопки — в main.mjs архива серии. Пример подготовлен без запуска. Числа в вычислениях ниже получены из формулы, а не из измерения интерфейса. Для продолжения пригодятся система координат документа и обратная матрица указателя из третьего урока.

Камера как отдельное состояние

Начальная камера имеет x = 0, y = 0, width = doc.width, height = doc.height. Рендерер передаёт эти четыре значения в viewBox. Документ при этом остаётся 1000 × 700. После приближения мы можем смотреть на область 500 × 350, но прямоугольник «Редактор» всё ещё имеет ширину 240 единиц.

Такое разделение особенно важно для сохранения и истории. Нажатие «Приблизить» не должно менять экспортированную схему или занимать запись отмены изменения рисунка. Камера хранится рядом с текущим выделением в main.mjs, а не внутри проверяемого документа. После загрузки нового файла интерфейс может заново показать страницу целиком.

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

Что означает коэффициент приближения

Кнопка увеличения передаёт коэффициент 0.8, уменьшения — 1.25. При увеличении уменьшается ширина видимой области, поэтому объекты занимают больше места на экране. Значение 0.8 не означает «умножить размеры фигур на 0.8». Оно применяется к камере, а геометрия фигур не меняется.

const width = Math.min(4000,
  Math.max(125, camera.width * factor));
const ratio = width / camera.width;

Это начало zoomCamera(). Ширина ограничена диапазоном от 125 до 4000 единиц. Нижняя граница не позволяет приблизить область до почти нулевого размера; верхняя ограничивает слишком далёкий обзор. Для высоты используем тот же ratio, сохраняя пропорции камеры. Если ширина уже достигла предела, отношение вычисляется после ограничения, поэтому положение не продолжает двигаться по прежнему коэффициенту.

При начальных 1000 × 700 увеличение даёт 800 × 560. Повторное увеличение — 640 × 448. Уменьшение с коэффициентом 1.25 возвращает предыдущую ширину, пока не действует ограничение. Дробные числа допустимы: камера не является пиксельной сеткой и не требует округления на каждом шаге.

Сохраняем точку под наблюдением

Одного изменения ширины недостаточно. Если оставить x и y прежними, приближение будет направлено к левому верхнему углу. Мы хотим сохранить заданную опорную точку в том же относительном месте изображения. Для этого расстояние от начала камеры до точки умножаем на новое отношение размеров.

return {
  x: point.x - (point.x - camera.x) * ratio,
  y: point.y - (point.y - camera.y) * ratio,
  width,
  height: camera.height * ratio
};

Это окончание функции. Возьмём начальную камеру и её центр 500, 350. При отношении 0.8 новая позиция равна 100, 70. Видимая область 800 × 560 снова имеет центр 500, 350. Получается приближение к центру страницы. Если использовать другую опорную точку, та же формула сохранит её положение относительно камеры.

В готовом интерфейсе кнопки используют центр текущей камеры. Мы не добавляем колёсико и жест двумя пальцами, чтобы не перехватывать обычную прокрутку страницы и не смешивать несколько операций ввода. Позднее можно получить точку под курсором через documentPoint() и передать её в ту же функцию. Но обработка колёсика, ограничение частоты и правила фокуса потребуют самостоятельного решения.

Сдвиг просмотра

Кнопки сдвига меняют camera.x на десять процентов её ширины. При полном виде это 100 единиц, при ширине 200 — 20. Такой шаг зависит от текущего масштаба, поэтому движение остаётся умеренным при приближении. Координаты фигур и массив слоёв не затрагиваются.

camera.x += sign * camera.width / 10;
redraw(false);

Здесь sign равен -1 или 1. Фрагмент вызывается обработчиками горизонтальных кнопок. Параметр false означает, что нужно перерисовать сцену без перестройки списка и панели свойств. Управление просмотра не меняет выбранную фигуру. В первой версии предусмотрен горизонтальный сдвиг; вертикальный можно добавить по той же формуле, но в архиве он не обещается.

Кнопка «Вся страница» заново задаёт камеру по текущему документу. Это важно после импорта файла другого размера. Если всегда возвращать жёстко записанные 1000 × 700, лист 1200 × 900 окажется частично вне кадра. Источник исходного масштаба — принятый документ, а не прежняя демонстрационная сцена.

Сдвиг камеры не ограничен левым и правым краями документа. Поэтому можно полностью вывести лист из кадра; для восстановления остаётся кнопка полного просмотра. Это сознательно простой вариант. Если ограничивать камеру, нужно сначала решить, допускается ли свободное пространство вокруг листа и как меняется правило при сильном удалении. Молчаливое ограничение только x диапазоном от нуля до ширины страницы не учитывает ширину самой камеры и может дать странные крайние положения.

Совместимость с вводом и экспортом

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

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

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

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