Дайте мне точку опоры…

от автора

…и я переверну землю.

Котик переворачивает землю. ЯПлакал

Котик переворачивает землю. ЯПлакал

Эту фразу приписывают Архимеду, так, во всяком случае, нам говорили в школе.

Иногда действительно требуется перевернуть мир, пусть и несколько в ином смысле.

Координатная система 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 растет вниз

Ось Y растет вниз

Как мы видим, ось 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.

Получилась замечательная картинка:

Ось Y растет вверх, как и задумано

Ось Y растет вверх, как и задумано

Прекрасно, давайте развивать решение. Добавим текста:

(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> 


ссылка на оригинал статьи https://habr.com/ru/articles/815863/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *