Экспорт изображения через Canvas
JSON позволяет продолжить редактирование, но для статьи или презентации чаще нужно изображение. В этом уроке добавим два формата: SVG и PNG. Оба строятся из документа, а не из снимка текущей области экрана. К концу урока разберём, почему приблизить редактор и увеличить разрешение экспорта — разные действия.
Функции находятся в files.mjs архива серии. Пример ещё не запускался; размеры и внешний вид файла ниже — ожидаемые. Мы экспортируем собственные простые фигуры без внешних изображений. Загрузка произвольных картинок, шрифтов или HTML потребовала бы отдельной обработки ресурсов и правил безопасности.
Полная страница вместо текущего просмотра
У камеры редактора могут быть координаты 100, 70 и размеры 800 × 560. Документ при этом остаётся 1000 × 700. Экспорт должен содержать весь лист. Поэтому exportSvg() создаёт новый SVG, задаёт ему ширину и высоту документа и вызывает существующий рендерер с камерой полного листа.
renderSvg(svg, doc, null, {
x: 0, y: 0,
width: doc.width,
height: doc.height
});
Это фрагмент векторного экспорта. Аргумент null снимает рабочее выделение. Затем XMLSerializer превращает дерево в строку, а Blob получает тип image/svg+xml. Поскольку рендерер использует проверенную модель и создаёт ограниченный набор элементов, сохранённая геометрия соответствует экранной по тем же правилам.
SVG остаётся векторным: геометрические элементы можно масштабировать без заранее выбранной пиксельной сетки. Однако текст с sans-serif зависит от доступного шрифта при открытии файла. Мы не переводим подписи в контуры и не внедряем шрифт. Если требуется полностью одинаковая типографика на всех устройствах, это отдельная задача экспорта, а не автоматическое свойство SVG.
Размер растрового изображения
PNG создаётся через новый, не показанный на странице Canvas. Его ширина и высота равны размерам документа, умноженным на коэффициент scale. Разрешаем два значения: один и два. Для страницы 1000 × 700 это даёт 1000 × 700 либо 2000 × 1400 пикселей. Значение devicePixelRatio текущего экрана здесь не участвует.
const canvas = document.createElement('canvas');
canvas.width = doc.width * scale;
canvas.height = doc.height * scale;
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('Canvas 2D недоступен');
ctx.scale(scale, scale);
После scale() рисуем в привычных единицах документа. Прямоугольник шириной 240 занимает 480 пикселей при коэффициенте два, но код геометрии продолжает использовать 240. Так экспорт не требует отдельного изменения всех данных. Для дробных размеров страницы фактические размеры буфера Canvas приводятся браузером к целым; учебный документ имеет целочисленные размеры.
На верхней границе страницы 2000 × 2000 коэффициент два создаёт буфер 4000 × 4000. Одни только четыре цветовых компонента на пиксель составляют около 64 миллионов байтов, без учёта дополнительных копий и сжатого результата. Поэтому даже наш небольшой набор размеров не следует считать бесплатным для памяти. Возможность экспорта больших листов нужно оценивать на целевых устройствах.
Рисуем по той же геометрии
Сначала заполняем весь лист белым. Затем идём по массиву фигур в обычном порядке. Прямоугольник рисуется через rect(), эллипс — через ellipse() с центром и радиусами из модели. После заполнения добавляем тонкую белую обводку, а затем подпись. Выделенную фигуру не выделяем особым цветом: экспорт относится к документу, а не к состоянию панели.
ctx.beginPath();
ctx.ellipse(
s.x + s.width / 2, s.y + s.height / 2,
s.width / 2, s.height / 2,
0, 0, Math.PI * 2
);
ctx.fill();
Это ветвь эллипса; полный цикл находится в exportPng(). beginPath() отделяет текущую фигуру от прежней. Если его забыть, последующая заливка или обводка может повторно затронуть накопленный путь. Для подписи задаём textAlign = 'center', textBaseline = 'middle' и 18px sans-serif.
SVG и Canvas используют разные механизмы текста, поэтому положение отдельных букв и сглаживание могут немного отличаться. Мы поддерживаем одинаковые координаты, содержание и базовое оформление, но не обещаем попиксельное совпадение двух форматов. Автоматический перенос, измерение текста и собственные шрифты в первой версии отсутствуют в обоих путях.
Асинхронное получение файла
Метод HTMLCanvasElement.toBlob() передаёт готовый Blob в callback. Оборачиваем его в Promise, чтобы использовать await. Проверяем null: отсутствие результата нельзя превращать в успешное сообщение о скачивании.
const blob = await new Promise(resolve => {
canvas.toBlob(resolve, 'image/png');
});
if (!blob) throw new Error('PNG не сформирован');
download(blob, 'schema.png');
Общий download() создаёт временный URL, активирует ссылку и позднее освобождает ресурс. Сообщение «PNG подготовлен» означает формирование и выдачу файла браузеру. Приложение не получает подтверждение сохранения на диск. Пользователь может отменить загрузку или выбрать другое имя, и это не является ошибкой рисования.
При вызове передаём копию текущего документа из истории. Даже если позднее пользователь изменит схему, уже начатый экспорт относится к своему снимку. Во время перетаскивания кнопки экспорта отключены, поэтому в файл не попадёт случайная промежуточная точка незаконченного жеста. Повторные независимые экспорты допустимы, но специальной очереди для них в первой версии нет.
Наш Canvas рисует только собственную геометрию и текст, поэтому не загружает сторонние изображения. При будущем добавлении картинки по внешнему URL появятся правила origin и возможность невозможного чтения загрязнённого Canvas. Нельзя просто вставить изображение в SVG и считать PNG-экспорт по-прежнему работающим. Нужно продумать загрузку ресурсов, ошибки, допустимые источники и соответствие модели.
Белый фон PNG выбирается явно. Если его убрать, свободные пиксели станут прозрачными, и рисунок может выглядеть иначе на тёмной странице. Формат документа пока не хранит настройку прозрачности. Поэтому текущий экспорт не предлагает скрытого выбора между белым и прозрачным листом. Добавление такого параметра должно одинаково отражаться в SVG и PNG.
Отдельно учитывайте качество масштабирования подписи. Коэффициент два увеличивает число пикселей и позволяет получить более подробный растр, но не добавляет новый размер текста в модель. Визуальное отношение подписи к фигуре сохраняется. Если вместо масштабирования контекста менять только размеры Canvas, рисунок остался бы маленьким в углу большого файла.
Что проверить самостоятельно
Приблизьте одну фигуру и выгрузите SVG: ожидается полный лист без рабочей обводки. Для исходной страницы сравните PNG с коэффициентами один и два: второй должен иметь удвоенные размеры по обеим сторонам. Геометрия относительно листа должна оставаться той же, а количество пикселей увеличится в четыре раза.
Затем поменяйте порядок слоёв и подпись, экспортируйте снова и сопоставьте их с JSON. Эти проверки пока не выполнены. Важно оценивать данные и размеры, а не только наличие скачанного файла. Мы получили редактируемый JSON, векторный SVG и растровый PNG. В последнем уроке соберём доступные способы управления, сообщения об ошибках и требования к передаче редактора следующему разработчику.