Позиционирование элементов внутри SVG очень похоже на абсолютное позиционирование элементов в HTML, но не идентично ему. Каждый элемент в SVG позиционируется “абсолютно” относительно области видимости SVG, а позиция внутри области видимости регулируется используемой системой координат. Но это сходство в позиционировании элементов не должно скрывать тот факт, что между элементами SVG и HTML есть фундаментально различие: у элементов SVG нет блочной модели, как у элементов HTML в CSS.

Перед тем, как продолжить, кратко рассмотрим блочную модель в CSS и как она влияет на позиционирование.

Краткий обзор блочной модели в CSS

Каждый элемент HTML обладает блочной моделью в CSS, складываемой из четырех блоков: контента, внутреннего отступа, границы и внешнего отступа.

блочная модель

Блочная модель элемента в CSS включает контент, внутренний отступ, границу и внешний отступ. Изображение взято из статьи о box-sizing справочника по CSS Codrops

Обычно размер элемента определяется свойствами ширины и высоты блока контента. Добавление любого внутреннего отступа увеличит рассчитываемую ширину или высоту элемента — именно так по умолчанию работает блочная модель при формировании его размеров. Свойство box-sizing позволяет вам контролировать формирование размеров элемента. Если конкретнее, то используя свойство box-sizing, вы можете приказать браузеру учитывать как часть размера элемента ширину внутреннего отступа и (или) ширину границы, после чего их изменение не будет влиять на фактические размеры элемента. Это полезно во многих случаях, особенно при создании сеточных систем с помощью CSS. Все об этом свойстве и его значениях вы можете узнать из справочника Codrops.

Блочная модель элемента также используется для создания контекста позиционирования для содержимого элемента, где это применимо, или для самого элемента.

Когда значение position элемента изменяется с дефолтного static, создается контекст позиционирования для его потомков или для него самого. Когда дефолтное позиционирование изменено, контекст позиционирования нужен, чтобы определить, где и как будет позиционирован элемент вне потока страницы (подробнее о позиционировании).

Если вы хотите удалить элемент из потока страницы, вы можете сделать это, позиционировав его абсолютно. Абсолютное позиционирование элемента означает, что он будет позиционирован относительно одного из своих предков, используя его блок в качестве контекста позиционирования.

Каждый контекст позиционирования требует системы координат. Она задается шириной и высотой блочной модели элемента. Любой потомок элемента будет позиционирован относительно элемента с помощью системы координат.

Однако в SVG есть единственная система координат, используемая по умолчанию для позиционирования элементов внутри области видимости — текущая система координат, заданная в атрибуте SVG viewBox. Когда элемент необходимо позиционировать внутри SVG, он позиционируется относительно всей области видимости SVG.

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

Индивидуальные элементы не обладают блочной моделью и, следовательно, не имеют собственной системы координат, которую можно было бы использовать в качестве контекста позиционирования для других элементов. Но что делать, если вы хотите позиционировать элемент SVG относительно другого элемента SVG или группы элементов?

Ответ: вложенные SVG.

Вложение SVG

Одной из моих любимых особенностей SVG является то, что это изображение, заданное разметкой. И эта разметка дает вам большие возможности по управлению и отображению изображением.

Вы можете вкладывать SVG в другой SVG. И потом поместить результат в следующий SVG и так далее, сколько хотите.

Вы можете вкладывать SVG настолько глубоко, насколько хотите. Количество уровней вложенности зависит от того, что вы хотите сделать и что вам нужно или не нужно. Лично я не сталкивалась с необходимостью вкладывать SVG больше, чем на два уровня.

<svg xmlns="http://www.w3.org/2000/svg">
    <!-- some SVG content -->
    <svg>
        <!-- some inner SVG content -->
    </svg>
 
    <svg>
        <!-- other inner SVG content -->
    </svg>
<svg>

Несколько замечаний о вложении SVG

  • Внутренний элемент <svg> не требует задания пространства имен (xmlns), так как по умолчанию предполагается, что он находится в том же пространстве имен, что и внешний <svg> . И даже внешний (корневой) <svg> не требует задания пространства имен, если он встроен в документ HTML5.
  • Вы можете использовать вложение SVG для группирования элементов и последующего позиционирования в родительском SVG. Конечно, вы можете группировать элементы внутри SVG, используя тег группировки <g>, но использование вместо этого <svg> имеет свои преимущества, такие как возможность задавать ширину и высоту всей группы, а также позиционировать с помощью абсолютных значений x и y вместо использования трансформаций. Задав ширину и высоту <svg>, вы ограничиваете содержимое <svg> границами области видимости (которая задается атрибутами width и height), весь контент выходящий за пределы будет обрезан.
  • Значения в процентах, заданные для элементов, вложенных в <svg> будут расчитываться относительно именно этого <svg>, а не корневого. В то же время значения в процентах, заданные внутреннему <svg> рассчитываются относительно корневого.

Зачем вкладывать <svg>?

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

Таким примером является следующий рисунок, изображающий маленькую птичку внутри яйца.

птичка внутри яйца

В обычном режиме при отзывчивости SVG, изменение размера экрана должно уменьшать весь SVG, при этом сохраняя позиционирование и пространственные соотношения между содержимым внутри него:

Изменение размера отзывчивого SVG в браузере

Изменение размера отзывчивого SVG в браузере, уменьшает SVG без воздействия на позиционирование и пространственные отношения между содержимым внутри него

Вкладывая элементы SVG, мы можем создавать отдельные “слои” внутри корневого <svg>, которые затем можно контролировать таким образом, что содержимое этих слоев будем менять позицию относительно корневого <svg> при изменении размеров области видимости. Делая так, мы можем показывать и прятать различные порции контента внутри SVG так, как мы хотим.

Например, мы можем разделить иллюстрацию из примера выше на 3 слоя так, чтобы показывать птичку на небольших экранах:

3 слоя в svg из первого примера

Этот эффект достигается путем использования в каждом из вложенных SVG разных значений preserveAspectRatio. Это обеспечивает “приклеивание” содержимого каждого SVG (т.е. каждого слоя) к корневому SVG, таким образом раскрывая содержимое между ними.

Я написала детальную статью о том, как добиться этого, вот она (пример с птицей из этой статьи).

Относительное позиционирование в SVG с помощью вложенных svg

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

Но как именно вложенный SVG дает нам возможность позиционирования элемента относительно другого элемента, не svg?

Перед тем как ответить на этот вопрос, нам надо понять, что является ограничивающий блоком (Bounding Box) у элементов SVG.

Что такое ограничивающий блок

Не все элементы SVG созданы равными. Мощь SVG в том, что базовые формы позволяют создавать все виды не прямоугольных форм: от произвольных линий до открытых или закрытых многоугольников, окружностей и эллипсов.

По причине природы всех этих элементов и отсутствию у них блочной модели CSS, в спецификации SVG введено понятие ограничивающего блока.

Ограничивающий блок элемента это наименьший прямоугольник, выровненный по осям системы координат элемента, вмещающий элемент и всех его потомков.

Проще говоря, ограничивающий блок это наименьший прямоугольник, который вы можете нарисовать вокруг элемента, замыкающий весь элемент, все его края и точки.

ограничивающий блок

Светло синий прямоугольник является наименьшим возможным прямоугольником вокруг спиральной формы, это визуальное представление формы ограничивающего блока. Выделяя элемент в графическом редакторе, вы можете извлечь свойства ограничивающего блока элемента. На скриншоте эти свойства показаны в панели трансформации Adobe Illustrator.

Три вида ограничивающих блоков могут быть рассчитаны для элемента:

  1. Ограничивающий блок объекта это блок, содержащий только геометрическую форму элемента.
  2. Ограничивающий блок обводки это блок, содержащий геометрическую форму элемента и форму обводки.
  3. Декорированный ограничивающий блок это блок, содержащий геометрическую форму элемента, форму обводки и маркеры.

Ограничивающий блок элемента характеризуется свойствами, которые могут быть извлечены при помощи метода getBBox() (эквивалент в SVG getBoundingClientRect(): x, y, width и height).

var svgElement = document.getElementById('el');
bbox = svgElement.getBBox();
 
console.log( bbox.x ) ;
console.log( bbox.y ) ;
console.log( bbox.width ) ;  
console.log( bbox.height ) ;

Используя ограничивающий блок элемента, мы можем подделать наличие системы координат вокруг элемента, с помощью чего мы сможем позиционировать другие элементы.

Более конкретно: мы будем создавать и использовать внутренний <svg> для установки новой системы координат вокруг элемента. Свойства <svg> будут определяться свойствами ограничивающего блока элемента — x, y, width и height.

Создание новой системы координат вокруг элемента SVG

Предположим, у нас есть следующее изображение SVG (позаимствовано с Vecteezy) с птицей и гнездом:

птица и гнездо

Итак, птица хочет попасть в гнездо. Мы можем поместить птицу над гнездом, задав позицию внутри SVG с помощью системы координат холста SVG.

Но в идеале, мы могли бы позиционировать ее с помощью процентных значений, рассчитываемых относительно блока “гнезда”. Мы можем имитировать это путем создания системы координат вокруг гнезда, используя новый элемент <svg>. У элемента <svg> есть своя система координат, задаваемая его высотой и шириной. Мы будем использовать эту систему координат, чтобы восполнить отсутствие системы координат у гнезда.

Затем мы переместим птицу в тег <svg>. При нахождении внутри <svg> позиция птицы будет рассчитываться относительно системы координат, установленной в этом <svg>.

Но для создания относительной связи между элементами птицы и гнезда, нам нужен контекст позиционирования птицы, находящейся в нашем <svg>, чтобы напомнить систему координат вокруг гнезда.

Чтобы добиться этого, мы позиционируем <svg> на вершине гнезда визуально. Важно заметить, что внутренний SVG на самом деле не оборачивает гнездо — составные части гнезда не находятся внутри тега <svg>. Мы только позиционируем наш <svg> на вершине гнезда визуально, так чтобы казалось, что <svg> это визуальное представление системы координат гнезда.

Для определения сдвига позиции <svg> (его x и y внутри корневого SVG) и его измерений, мы будем использовать свойства ограничивающего блока гнезда.

Позиция <svg> — значения x и y, будут равны значениям x и y ограничивающего блока гнезда. Имеется в виду ограничивающий блок группы элементов, формирующих гнездо (у групп также как и у отдельных элементов могут быть ограничивающие блоки). У внутреннего SVG есть явно заданные ширина и высота, равные ширине и высоте ограничивающего блока гнезда.

Вот как это выглядит:

Ограничивающий блок гнезда

Но на изображении выше птица еще расположена в левом верхнем углу. Вот как она накладывается на изображение гнезда:

Наложение птицы на гнездо

Серая граница показывает ограничивающий блок, а также новую систему координат вокруг гнезда, установленную SVG.

Важно отметить, что птица сейчас позиционирована относительно системы координат внутреннего <svg>. Обратите внимание, как она сдвигается на некоторое количество пикселей с верхнего левого угла внутреннего SVG, так же как ранее она сдвигалась относительно левого верхнего угла корневого SVG. Это нормально пока. Мы должны избавится от этих отступов, чтобы получить больше контроля над позиционированием птицы. Скоро мы к этому перейдем.

Также стоит обратить внимание на то, что хотя внутреннему SVG явно заданы ширина и высота ограничивающего блока гнезда, лапки птицы обрезаны потому что она так позиционирована. Если в ваших проектах есть другие элементы или больше элементов, с ними может произойти то же самое. Это совсем не то, что вам нужно. Чтобы разобраться с этим, вам надо явно задать стиль overflow: visible для внутреннего SVG. Это гарантирует, что внутренний SVG будет вести себя только как контекст позиционирования, а не как контейнер, ограничивающий отображение содержимого в этой области.

Вот как выглядит код:

<svg id="birds" xmlns="http://www.w3.org/2000/svg" width="100%" viewBox="0 0 3945.8 2400">
    <title>Bird & Nest</title>
    <g id="nest">
      <path ...>
      <!-- ... -->
    </g>
    <!-- The ID I'm giving this SVG is just for demonstration purposes -->
    <svg x="698" y="1219" width="1055" height="641" viewBox="0 0 1055 641" style="overflow: visible;" id="coord-sys">
        <g id="bird">
          <path ...>
          <!-- ... -->
        </g>
    </svg>
</svg>

Также как у корневого SVG, значение viewBox внутреннего SVG с #coord-sys определяется его размерами.

Далее нам надо позиционировать птицу внутри новой системы координат. Я больше не буду упоминать в тексте термин “внутренний SVG”, в соответствии с кодом он у нас svg#coord-sys.

Так как мы позиционируем птицу внутри svg#coord-sys, нам нужна возможность задать позицию для всей группы элементов, формирующих птицу, ведь она не просто один элемент, а группа форм. И мы позиционируем группу, а не один элемент. Группа элементов, формирующих птицу, обернута тегом <g>.

В чем наша проблема — у элемента <g> нет атрибутов x и y . Поэтому мы не можем просто сдвинуть его на определенную позицию с их помощью:

<g id="bird" x="50%" y="50%">

Обычно для перемещения группы элементов внутри SVG, мы используем функции для трансформации SVG или CSS (трансформацию преобразования, в большинстве случаев). Вы, конечно, можете использовать трансформацию для сдвига группы. Но это сведет на нет то, чего мы пытаемся достичь и сделает новую систему координат бесполезной. В итоге мы могли бы использовать трансформации для позиционирования птицы рядом с гнездом внутри корневого SVG без создания новой системы координат.

А мы хотим имитировать позиционирование элементов в CSS относительно друг друга. Так сказать “сдвинуть группу элементов на позицию (x, y) внутри соответствующего контекста позиционирования”.

Так как у <g> нет атрибутов x и y, мы заменим его другим <svg>.

<svg id="birds" xmlns="http://www.w3.org/2000/svg" width="100%" viewBox="0 0 3945.8 2400">
    <title>Bird & Nest</title>
    <!-- ... -->
    <svg x="698" y="1219" width="1055" height="641" viewBox="0 0 1055 641" style="overflow: visible;">
        <svg id="bird">
          <!-- ... -->
        </svg>
    </svg>
</svg>

SVG, оборачивающему птицу, задан ID bird. Этот svg в противоположность своему предку, будет работать исключительно контейнером, даже несмотря на то, что он создает новую систему координат — мы не будем ею пользоваться. Используя этот svg, мы теперь можем сдвинуть птицу внутри новой системы координат, установленной вокруг (на вершине) гнезда.

Теперь пришло время удалить лишнее пустое пространство рядом с птицей. svg#bird обладает теми же размерами и областью видимости, что и оборачивающий его svg#coord-sys — это значит, что при сдвиге птицы нам надо учитывать это пустое пространство. И если мы захотим сдвинуть птицу в левый верхний угол системы, мы не можем просто задать x и y равными нулю — чтобы добиться этого, нам нужен отрицательный отступ в обоих направлениях. Это не практично, ведь нам придется учитывать этот сдвиг каждый раз при позиционировании птицы.

А вот теперь вам надо не только что-то слышать о viewBox, но и понимать, как он работает. Если у вас с этим проблемы, уделите время ознакомлению с этой статьей.

Мы изменим значение viewBox в svg#bird для обрезки пустого пространства. Мы собираемся использовать систему координат svg#bird, но только совсем немного.

По умолчанию, вложенный SVG занимает 100% ширины и высоты контейнера, если вы явно не зададите иное.

Поэтому у svg#bird сейчас такие же размеры, как и у svg#coord-sys (на изображении ниже у него розовая рамка):

Сравнение размеров вложенного SVG и контейнера

Нам не нужно, чтобы размеры отличались в этом примере, поэтому мы оставим их как есть.

Изображение выше также показывает, количество пустого пространства, на которое птица сдвигается внутри svg. Чтобы отменить сдвиг, мы изменим значение viewBox в svg#bird для обрезки пустого пространства.

<svg id="birds" xmlns="http://www.w3.org/2000/svg" width="100%" viewBox="0 0 3945.8 2400">
    <!-- ... -->
    <svg x="698" y="1219" width="1055" height="641" style="overflow: visible;">
        <svg id="bird" viewBox="150 230 1055 641">
          <path ...>
          <!-- ... -->
        </svg>
    </svg>
</svg>

Это сдвинет птицу так, что она будет расположена в верхнем левом углу системы координат. Я уберу фокус с svg#bird на следующем изображении, там будет показана только система координат гнезда и новое место птицы внутри нее.

Птица расположена в левом верхнем углу системы координат

Теперь, когда птица расположена в левом верхнем углу контейнера, мы можем передвигать ее и получать каждый раз ожидаемый результат. Например, мы можем сдвинуть ее на 50% в обоих направлениях:

<svg id="bird" style="overflow: visible" viewBox="150 230 1055 641" x="50%" y="50%">

И получим следующий результат:

птица позиционирована на 50% по осям полученной системы координат

С такой настройкой, мы можем передвигать птицу внутри координат гнезда, также как один элемент HTML внутри другого элемента HTML с помощью CSS. Одинаково работают относительные и абсолютные значения.

Неплохо, да? Это максимально близкий к относительному позиционированию результат, которого мы може добиться в SVG на данный момент.

Да, это не самый простой процесс, но если у вас есть хорошее понимание системы координат SVG и работы viewBox, то это не так уж и сложно.

Вот работающее демо примера с птицей и гнездом, птица размещена на краю гнезда:

See the Pen AXRRZd by prgssr (@prgssr) on CodePen.

Заключение

В этой статье использован очень специфичный пример и, надо признать, это не самый частый случай на практике. У вас могут быть ситуации совершенно отличные от описанной. Вы можете работать с SVG, в которых вам совсем не понадобиться обрезка viewBox. Если вы создаете SVG самостоятельно, вы можете позиционировать свой элемент (типа птицы виз моего примера) в левом верхнем углу холста SVG и при обертывании другим SVG, он будет также позиционироваться в левом верхнем углу и вам не надо будет ничего обрезать. Я сделала этот пример немного более сложным, чтобы покрыть большую часть возможных сценариев (и потому что мне было немного лень редактировать SVG в Illustrator после написания половины статьи).

Главное понять, как имитировать относительное позиционирования с помощью вложенных SVG. Этот принцип не зависит от того, сколько уровней вложенности вы используете: один, два или больше.

Эта техника полезна для позиционирования элементов пользовательского интерфейса в формате SVG относительно друг друга. Или для относительного позиционирования динамически создаваемых SVG. Вы ограничены лишь вашим воображением.