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

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

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

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

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

Координатная система 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 к тегу :

(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) "Привет!"))

У тега нет своих координат 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 градусов, то есть можно производить больше одной трансформации или вообще задать матрицу преобразований.

f174daf144422fe688462e5ce4270dfa.png

Параметры viewPort

Последнее замечание сделаю относительно значений параметра viewPort тега .

Я долго не мог сформулировать, как им пользоваться, пока не придумал такое объяснение.

Окно просмотра характеризуется двумя парами значений. Первая — положение окна просмотра в пользовательских координатах. По сути это просто точка на чертеже в пользовательских координатах.

Вторая пара задает количество точек на чертеже, которые мы видим в окно просмотра, соответственно по горизонтали и вертикали, но при этом само окно имеет размер, задаваемый параметрами ширины и высоты тега

То есть, viewPort имеет фиксированный размер на экране или в браузере, а вот содержимое его подстраивается с учетом коэффициентов, получаемых соотношением размеров из окна просмотра и тега Кому как, а мне так стало понятнее.

Пропуск параметров

Ранее мы уже видели, что у тега должны обязательно быть указаны какие‑то значения.

В библиотеке подумали о том, что иногда можно опустить проверку параметров. Для этого используется макрос (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 файл




  
  
  
    
  
  
    
    
  
  
    
      
        Привет
      
    
    
      
        Мир
      
    
    
      
        SVG
      
    
  

© Habrahabr.ru