...и я переверну землю.
Эту фразу приписывают Архимеду, так, во всяком случае, нам говорили в школе.
Иногда действительно требуется перевернуть мир, пусть и несколько в ином смысле.
Координатная система SVG
Понадобилось мне сохранить векторный рисунок. Для этого SVG подходит как нельзя лучше — генерируется просто, размер небольшой, структура понятна и предсказуема.
Вот только незадача, координата Y в нем идет не так, как принято в математике — привычная нам ось идет вверх, тогда как в SVG — вниз. Пробуем рисовать. Будем использовать библиотеку cl‑svg.
В Quicklisp устаревшая версия, поэтому имеет смысл склонировать ее себе в ~/quicklisp/local-projects/ и выполнить
(ql:register-local-projects)
.
Итак, делаем простейший файл, рисуем координатные оси, ну или их какое-то подобие.
(in-package #:svg)
(with-svg-to-file
(scene 'svg-1.1-toplevel :height 230 :width 230)
(#p"~/devel/svg/coords.svg" :if-exists :supersede)
(draw scene (:line :x1 0 :y1 -10 :x2 0 :y2 200))
(draw scene (:line :x1 -10 :y1 0 :x2 200 :y2 0)))
Запускаем, получился файл coord.svg
Запускаем его и... ничего.
Правильно, потому что по стандарту линия не рисуется, если у нее не задано значение цвета пера (stroke
). Кроме того, у нас не задано значение поля viewPort
.
Корректный вариант такой (далее первую форму (in-package #:svg)
опускаю):
(with-svg-to-file
(scene 'svg-1.1-toplevel
:height 230
:width 230
:view-box "-20 -20 230 230")
(#p"~/devel/svg/coords.svg" :if-exists :supersede)
(draw scene
(:rect :x -20 :y -20 :height 230 :width 230)
:fill "#EEEEEE" :stroke "none")
(draw scene
(:line :x1 0 :y1 -10 :x2 0 :y2 200) :stroke "green")
(draw scene
(:line :x1 -10 :y1 0 :x2 200 :y2 0) :stroke "green"))
Линии осей нарисованы зеленым цветом.
Для удобства восприятия добавлен прямоугольник под картинку.
Получилось что-то такое:
Как мы видим, ось Y растет вниз.
Для того, чтобы перевернуть ось, можно просто нарисовать ось X снизу, а остальные расчеты просто вести с поправкой на то, что координаты перевернуты. Решение вполне себе рабочее, но придется внести изменения в расчетную часть, а она может быть нетривиальной и такое не всегда просто реализовать. Кроме того, наверняка появится множество краевых случаев, которые нужно будет обрабатывать.
Попробуем другой метод.
Использование CSS
SVG позволяет использовать стилевую информацию. Этим мы и воспользуемся.
Добавим стиль, применяющий масштаб -1
к тегу <svg>
:
(with-svg-to-file
(scene 'svg-1.1-toplevel :height 230 :width 230 :view-box "-20 -20 230 230")
(#p"~/devel/svg/coords.svg" :if-exists :supersede)
(style scene "svg#toplevel {display:flex; transform: scaleY(-1);}")
(draw scene
(:rect :x -20 :y -20 :height 230 :width 230)
:fill "#EEEEEE" :stroke "none")
(draw scene
(:line :x1 0 :y1 -10 :x2 0 :y2 200) :stroke "green")
(draw scene
(:line :x1 -10 :y1 0 :x2 200 :y2 0) :stroke "green"))
Идентификатор #toplevel
создается внутри класса svg-1.1-toplevel
.
Получилась замечательная картинка:
Прекрасно, давайте развивать решение. Добавим текста:
(with-svg-to-file
(scene 'svg-1.1-toplevel :height 230 :width 230 :view-box "-20 -20 230 230")
(#p"~/devel/svg/coords.svg" :if-exists :supersede)
(style scene "svg#toplevel {display:flex; transform: scaleY(-1);}")
(draw scene
(:rect :x -20 :y -20 :height 230 :width 230)
:fill "#EEEEEE" :stroke "none")
(draw scene
(:line :x1 0 :y1 -10 :x2 0 :y2 200) :stroke "green")
(draw scene
(:line :x1 -10 :y1 0 :x2 200 :y2 0) :stroke "green")
(text scene (:x 20 :y 20) "Привет!"))
Ооочень интересно. Перевернут не только мир, но и текст.
Это забавно, но логично. Мы же весь холст перевернули.
Хорошо, давайте теперь применим масштабирование и к тексту:
(with-svg-to-file
(scene 'svg-1.1-toplevel :height 230 :width 230 :view-box "-20 -20 230 230")
(#p"~/devel/svg/coords.svg" :if-exists :supersede)
(style scene "svg#toplevel {display:flex; transform: scaleY(-1);}")
(style scene "svg#toplevel > text {transform: scaleY(-1);}")
(draw scene
(:rect :x -20 :y -20 :height 230 :width 230)
:fill "#EEEEEE" :stroke "none")
(draw scene
(:line :x1 0 :y1 -10 :x2 0 :y2 200) :stroke "green")
(draw scene
(:line :x1 -10 :y1 0 :x2 200 :y2 0) :stroke "green")
(text scene (:x 20 :y 20) "Привет!"))
Текст перевернулся, но теперь он расположен непонятно где:
Все дело в том, что преобразование scaleY(-1)
выполняется относительно начала координат, а не относительно координат элемента, к которому это преобразование применяется. Получается, что базовая линия шрифта находится на координате Y, равной 20, и после применения масштабирования расположено также на 20 пикселей от оси, но в другую сторону.
Можно применить дополнительные преобразования — сначала перенести текст к началу координат, затем масштабировать, затем отнести снова назад. Но это нужно рассчитывать, давайте сделаем по‑другому.
Обернем текст в группу и потом применим преобразование к тексту.
(with-svg-to-file
(scene 'svg-1.1-toplevel :height 230 :width 230 :view-box "-20 -20 230 230")
(#p"~/devel/svg/coords.svg" :if-exists :supersede)
(style scene "svg#toplevel {display:flex; transform: scaleY(-1);}")
(style scene "svg#toplevel g > text {transform: scaleY(-1);}")
(draw scene
(:rect :x -20 :y -20 :height 230 :width 230)
:fill "#EEEEEE" :stroke "none")
(draw scene
(:line :x1 0 :y1 -10 :x2 0 :y2 200) :stroke "green")
(draw scene
(:line :x1 -10 :y1 0 :x2 200 :y2 0) :stroke "green")
(make-group scene
(:transform "translate(20, 20)")
(text* (:x 0 :y 0) "Привет!")))
Получилось то, что нужно!
Ключевым здесь являются вызовы
(style scene "svg#toplevel g > text {transform: scaleY(-1);}")
...
(make-group scene
(:transform "translate(20, 20)")
(text* (:x 0 :y 0) "Привет!"))
У тега <g>
нет своих координат x
и y
но к нему можно применить преобразование transform()
и в нем задать нужное смещение. Кроме того, координаты x
и y
для текста можно не указывать, но библиотека cl-svg
требует, чтобы они были указаны, поэтому вписываем. Позже мы исправим и это.
Вложенные группы
Как частное решение это вполне сойдет. Но для более сложного документа лучше будет сгруппировать элементы логически. Это сделает документ структурированным и более удобным в ряде случаев, например при работе в консоли браузера.
(with-svg-to-file
(scene 'svg-1.1-toplevel :height 230 :width 230 :view-box "-20 -20 230 230")
(#p"~/devel/svg/coords.svg" :if-exists :supersede)
(style scene "svg#toplevel {display:flex; transform: scaleY(-1);}")
(style scene "svg#toplevel g > text {transform: scaleY(-1);}")
(make-group scene (:id "frame")
(draw*
(:rect :x -20 :y -20 :height 230 :width 230)
:fill "#EEEEEE" :stroke "none"))
(make-group scene (:id "axes")
(draw*
(:line :x1 0 :y1 -10 :x2 0 :y2 200) :stroke "green")
(draw*
(:line :x1 -10 :y1 0 :x2 200 :y2 0) :stroke "green"))
(let ((texts (make-group scene (:id "texts"))))
(make-group texts (:transform "translate(20, 20)")
(text* (:x 0 :y 0) "Привет"))
(make-group texts (:transform "translate(30, 40)")
(text* (:x 0 :y 0) "Мир"))
(make-group texts (:transform "translate(30, 60) rotate(90)")
(text* (:x 0 :y 0) "SVG"))))
В последнем случае дополнительно текст повернут на 90 градусов, то есть можно производить больше одной трансформации или вообще задать матрицу преобразований.
Параметры viewPort
Последнее замечание сделаю относительно значений параметра viewPort
тега <svg>
.
Я долго не мог сформулировать, как им пользоваться, пока не придумал такое объяснение.
Окно просмотра характеризуется двумя парами значений. Первая — положение окна просмотра в пользовательских координатах. По сути это просто точка на чертеже в пользовательских координатах.
Вторая пара задает количество точек на чертеже, которые мы видим в окно просмотра, соответственно по горизонтали и вертикали, но при этом само окно имеет размер, задаваемый параметрами ширины и высоты тега <svg>
То есть, viewPort
имеет фиксированный размер на экране или в браузере, а вот содержимое его подстраивается с учетом коэффициентов, получаемых соотношением размеров из окна просмотра и тега <svg>
Кому как, а мне так стало понятнее.
Пропуск параметров
Ранее мы уже видели, что у тега должны обязательно быть указаны какие‑то значения.
В библиотеке подумали о том, что иногда можно опустить проверку параметров. Для этого используется макрос (without-attribute-check ... )
. Финальный код этого учебного материала выглядит так:
(with-svg-to-file
(scene 'svg-1.1-toplevel :height 230 :width 230 :view-box "-20 -20 230 230")
(#p"~/devel/svg/coords.svg" :if-exists :supersede)
(style scene "svg#toplevel {display:flex; transform: scaleY(-1);}")
(style scene "svg#toplevel g > text {transform: scaleY(-1);}")
(make-group scene (:id "frame")
(draw*
(:rect :x -20 :y -20 :height 230 :width 230)
:fill "#EEEEEE" :stroke "none"))
(make-group scene (:id "axes")
(draw*
(:line :x1 0 :y1 -10 :x2 0 :y2 200) :stroke "green")
(draw*
(:line :x1 -10 :y1 0 :x2 200 :y2 0) :stroke "green"))
(without-attribute-check
(let ((texts (make-group scene (:id "texts"))))
(make-group texts (:transform "translate(20, 20)")
(text* () "Привет"))
(make-group texts (:transform "translate(30, 40)")
(text* () "Мир"))
(make-group texts (:transform "translate(30, 60) rotate(90)")
(text* () "SVG")))))
Заключение
Мы рассмотрели очень небольшой кусочек кода, позволяющий создать SVG с помощью языка Common Lisp, а также перевернуть мир, пусть и в отдельно взятом SVG-файле.
Получившийся SVG файл
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg viewBox="-20 -20 230 230" width="230" height="230" version="1.1"
id="toplevel" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<style>
svg#toplevel {display:flex; transform: scaleY(-1);}
</style>
<style>
svg#toplevel g > text {transform: scaleY(-1);}
</style>
<g id="frame">
<rect x="-20" y="-20" height="230" width="230" fill="#EEEEEE" stroke="none"/>
</g>
<g id="axes">
<line x1="0" y1="-10" x2="0" y2="200" stroke="green"/>
<line x1="-10" y1="0" x2="200" y2="0" stroke="green"/>
</g>
<g id="texts">
<g transform="translate(20, 20)">
<text>
Привет
</text>
</g>
<g transform="translate(30, 40)">
<text>
Мир
</text>
</g>
<g transform="translate(30, 60) rotate(90)">
<text>
SVG
</text>
</g>
</g>
</svg>
firstmixon
Откройте для себя учебник по компьютерной графике, ось Y направленна везде вниз, это стандарт в компьютерной графике.
DmitrySolomennikov Автор
То есть к Lisp реализации вопросов не возникло? Очень хорошо!
Стандарт, но не везде.
firstmixon
По вашей ссылке "Device space", это стандарт, а первое это как принято к примеру в черчение.