Структурирование, группировка и привязка в SVG — элементы <g>, <use>, <defs> и <symbol>

http://sarasoueidan.com/blog/structuring-grouping-referencing-in-svg/
  • Перевод
SVG предоставляет возможности для структурирования документа посредством специальных элементов, которые позволяют определять и группировать объекты, а также ссылаться на них в дальнейшем. Они упрощают повторное использование кода, сохраняя его чистым и читаемым. В данной статье будут рассмотрены эти элементы, а также их различия и преимущества.

Группировка с использованием элемента <g>


Элемент <g> используется для логической группировки набора связанных графических элементов. Это можно сравнить с группировкой объектов в графических редакторах.

Элемент <g> объединяет в группу все свое содержимое. Как правило, ему задается идентификатор, по которому будет производиться обращение в дальнейшем. Любые стили, применяемые к элементу <g>, будут также применены ко всем его потомкам. Это позволяет задавать стили и преобразования, а также добавлять интерактивность и анимацию сразу целой группе объектов.

В качестве примера будем использовать нарисованную в SVG птичку. Она состоит из нескольких отдельных фигур, описываемых кругами и путями. [По невыясненным причинам автором описаны круги путями вместо использования circle, хоть и было обещано использовать круги. Не спрашивайте почему. Я это исправил. Не спрашивайте зачем. — Пер.]

Код, рисующий птичку
<svg width="1144.12px" height="400px" viewBox="0 0 572.06 200">
	<style>
		svg{background-color:white;}
		#wing{fill:#81CCAA;}
		#body{fill:#B8E4C2;}
		#pupil{fill:#1F2600;}
		#beak{fill:#F69C0D;}
		.eye-ball{fill:#F6FDC4;}
	</style>
	<g id="bird">
		<g id="body">
			<path d="M48.42,78.11c0-17.45,14.14-31.58,31.59-31.58s31.59,14.14,31.59,31.58c0,17.44-14.14,31.59-31.59,31.59S48.42,95.56,48.42,78.11"/>
			<path d="M109.19,69.88c0,0-8.5-27.33-42.51-18.53c-34.02,8.81-20.65,91.11,45.25,84.73c40.39-3.65,48.59-24.6,48.59-24.6S124.68,106.02,109.19,69.88"/>
			<path id="wing" d="M105.78,75.09c4.56,0,8.84,1.13,12.62,3.11c0,0,0.01-0.01,0.01-0.01l36.23,12.38c0,0-13.78,30.81-41.96,38.09c-1.51,0.39-2.82,0.59-3.99,0.62c-0.96,0.1-1.92,0.16-2.9,0.16c-15.01,0-27.17-12.17-27.17-27.17C78.61,87.26,90.78,75.09,105.78,75.09"/>
		</g>
		<g id="head">
			<path id="beak" d="M50.43,68.52c0,0-8.81,2.58-10.93,4.86l9.12,9.87C48.61,83.24,48.76,74.28,50.43,68.52"/>
			<circle class="eye-ball" cx="72" cy="71.5" r="11"/>
			<circle id="pupil" cx="72" cy="71.5" r="7"/>
			<circle class="eye-ball" cx="77" cy="74" r="5"/>
		</g>
	</g>
</svg>
Пример на codepen

Допустим, вы рисуете такую птичку в Illustrator. Если нужно переместить ее из одного места в другое, то группировка объектов упрощает эту задачу, так как позволяет не перемещать каждый элемент по отдельности. Группировка в SVG делает примерно то же самое. В примере выше мы помимо общей группы для всей птички (id="bird") выделили также подгруппы, определяющие отдельно голову и тело (id="body", id="head").

Если вы измените цвет заливки группы #body, изменится заливка всех элементов внутри группы. Это может быть очень удобно. [Речь об элементах, у которых цвет заливки не задан явно. Например, в том же #body элемент #wing имеет свой цвет. — Пер.]

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

Так, в случае со сгруппированной птицей можно отмасштабировать ее всего одной строкой CSS:
#bird {transform: scale(2);}

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

Элемент <g> имеет еще одну важную и интересную особенность: он может содержать теги <title> и <desc>, которые позволят изображению быть обработанным Screen Reader'ами [Программы воспроизведения текста для людей с ограниченными возможностями; не нашел емкого и адекватного перевода термина. — Пер.]; также они позволяют сделать код более читаемым для человека. Пример использования:
<g id="bird">
	<title>Bird</title>
	<desc>An image of a cute little green bird with an orange beak.</desc>
	<!-- ... -->
</g>


Повторное использование элементов с помощью <use>


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

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

Элемент <use> принимает в качестве атрибутов координаты x и y, высоту (height), ширину (width) и ссылку на исходный элемент (xlink:href). В качестве ссылки выступает идентификатор объекта.

Например, если мы хотим добавить еще одну птичку, вместо копирования ее кода можно использовать тег <use>:
<use x="100" y="100" xlink:href="#bird" />
Пример на codepen

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

Допустим, наша птичка была создана в отдельном файле animals.svg. В этом случае ссылаться на нее можно так:
<use x="100" y="100" xlink:href="path/to/animals.svg#bird" />

Нужно учитывать, что ссылки на внешние SVG в <use> не работают в большинстве версий IE (до IE 11). Рекомендую ознакомиться со статьей от Chris Coyier, в коей рассказывается об обходе этого ограничения.

Самые внимательные читатели наверняка заметили, что координаты, задаваемые элементу <use> отсчитываются не от начала координат всего SVG-изображения. На самом деле это сокращенная форма записи атрибута transform. Следующие две строчки являются эквивалентными:
<use x="100" y="100" xlink:href="#bird" />
<use xlink:href="#bird" transform="translate(100, 100)" />

image

Проще говоря, координаты элемента <use> задаются относительно исходного элемента. Такое поведение не всегда оптимально и может быть недостатком.

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

Однако вы все-таки можете применить независимое преобразование к элементу <use>. Например, следующая строка кода позволяет повторно использовать птичку, размеры которой будут составлять лишь половину от размеров исходной:
<use x="100" y="100" xlink:href="#bird" transform="scale(0.5)" />

[При этом принцип работы системы координат может показаться несколько неожиданным. Она также масштабируется. Если исходный элемент был спозиционирован в 100 пикселях от края изображения, то такая его копия будет расположена в 50 пикселях от края. На задаваемые x и y это тоже распространяется. Таким образом, слова о расположении копии относительно исходного элемента не совсем верны. — Пер.]

В отличие от преобразований, переопределить стили копии нельзя. Таким образом, если вы захотите создать армию птичек разного цвета, то использовать для этого <use> не получится (если только исходный элемент не определен внутри <defs> без своих стилей, но об этом в следующем разделе).

Элемент <use> позволяет повторно использовать элемент, который уже отображается на SVG-изображении. Если же вы хотите просто определить элемент, не отображая его, а затем отрисовать в нужном месте, когда это потребуется, на помощь придет элемент <defs>.

Повторное использование хранимых элементов с помощью <defs>


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

Храниться в <defs> может что угодно, начиная с группы элементов, вроде нашей птички, и заканчивая маской или градиентом. Это шаблон для дальнейшего использования. Сам по себе он никогда не отображается, только использующие его сущности.

Ниже приведен пример, в котором градиент сначала определяется, а затем используется для заливки прямоугольника:
Код, рисующий прямоугольник, залитый градиентом
<svg>
	<defs>
		<linearGradient id="gradient">
			<stop offset="0%" style="stop-color: deepPink"></stop>
			<stop offset="100%" style="stop-color: #009966"></stop>
		</linearGradient>
	</defs>
	<rect width="100" height="100" stroke="#eee" stroke-width="5" fill="url(#gradient)"></rect>
</svg>
Пример на codepen

[Автором допущена досадная ошибка: не были указаны высота и ширина прямоугольника. — Пер.]

Определение линейного градиента внутри <defs> гарантирует, что он не будет внезапно отображен сам по себе, только при использовании где-либо.

В предыдущем разделе было упомянуто два недостатка элемента <use>:
  1. Положение нового элемента задается относительно исходного.
  2. Стили исходного элемента не могут быть переопределены в копиях.

Ну и к тому же исходный элемент отображается сам по себе.

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

В следующем примере мы будем рисовать дерево [И думать забудьте о двоичном! — Пер.]. Оно состоит из ствола и листьев. Листья собраны в группу с id="leaves", а вместе со стволом они входят в более общую группу с id="tree".
Часть кода, рисующего дерево
<svg width="500.79px" height="200px" viewBox="0 0 500.79 200">
	<style type="text/css">
		#leaves{fill:#8CC63F;}
		#bark{fill:#A27729;}
	</style>
	<g id="tree">
		<path id="bark" d="M91.33,165.51c0,0,4.18-27.65,1.73-35.82l-18.55-25.03l3.01-2.74l17.45,19.87l1.91-37.6h4.44l1.83,24.53
		l15.26-16.35l3.27,4.36l-16.07,19.34c0,0-2.72,0-1.09,19.34c1.63,19.34,3,29.7,3,29.7L91.33,165.51z"/>
		<g id="leaves">
			<path class="leaf" d="M96.97,79.07c0,0-14.92,4.34-23.52-14.05c0,0,19.4-7.98,24.37,11.9c0,0-9.68-3.57-13.07-6.73
			C84.75,70.2,91.82,77.99,96.97,79.07z"/>
			<path class="leaf" d="M74.07,100.91c0,0-15.94-1.51-17.2-22.39c0,0,21.62-0.27,18.83,20.66c0,0-7.92-7.1-9.97-11.41
			C65.73,87.77,69.55,97.92,74.07,100.91z"/>
			<!-- ... -->
		</g>
	</g>
</svg>

На выходе наше дерево будет выглядеть как-то так:
image

[К сожалению, код был сокращен автором, потеряв большую часть листьев. То, что осталось, напоминает мне часть какого-то инопланетного индустриального пейзажа. — Пер.]

Если обернуть группу #tree в элемент <defs>, дерево перестанет отображаться.
Все больше вырезанного кода
<svg width="500.79px" height="200px" viewBox="0 0 500.79 200">
	<style type="text/css">
		#leaves{fill:#8CC63F;}
		#bark{fill:#A27729;}
	</style>
	<defs>
		<g id="tree">
			<!-- ... -->
		</g>
	</defs>
</svg>

Теперь дерево служит в качестве шаблона. Мы можем использовать его с помощью элемента <use>, как и любой другой элемент. Единственное отличие заключается в том, что координаты x и y задаются относительно начала координат.

В качестве примера нарисуем мини-лес.
<use xlink:href="#tree" x="50" y="100" />
<use xlink:href="#tree" x="200" y="100" />
<use xlink:href="#tree" x="350" y="100" />

image

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

При повторном использовании элементов, определенных в <defs>, вы можете применять отдельных стили для каждого дерева, пока эти стили не определены для исходного шаблона. Если же дерево внутри <defs> будет иметь свои стили, то переопределить их будет нельзя. Таким образом, <defs> прекрасно подходит для того, чтобы определить самую основу, которая будет использована и раскрашена при необходимости. Используя только <use> без <defs>, добиться такой гибкости было бы невозможно.

Обратите внимание, что элементы внутри <defs> не отображаются, то есть ведут себя точно так же, как элемент <g> с установленным свойством display="none". Тем не менее, потомки <defs> всегда представлены в исходном дереве [Речь о дереве объектов SVG, полагаю. — Пер.], и на них всегда могут сослаться другие элементы. Таким образом, значение свойства display элемента <defs> (или его потомков) не мешает другим элементам ссылаться на него, даже если это свойство установлено в none.

[Не уверен в правильности понимания того, что хотел сказать автор в данном абзаце. Я пробовал ставить display="none" для элемента внутри <defs>, и этот элемент переставал отображаться, что вроде бы логично. Видимо, речь о том, что самому <defs> можно поставить display="none", и ничего не изменится. В следующем разделе это описано более внятно. На всякий случай добавлю абзац в оригинале. — Пер.]

Предыдущий абзац в оригинале
Note that elements inside the <defs> element are prevented from becoming part of the rendering tree just as if the defs element were a g element and the display property were set to none. However, that the descendants of a defs are always present in the source tree and thus can always be referenced by other elements; thus, the value of the display property on the defs element or any of its descendants does not prevent those elements from being referenced by other elements, even if it is set to none.


Группировка элементов с использованием <symbol>


Элемент <symbol> похож на <g>: он также предоставляет возможность группировать элементы. Можно выделить два основных отличия:
  1. Элемент <symbol> не отображается сам по себе. Этим он похож на <defs>.
  2. Элемент <symbol> может иметь собственные атрибуты viewBox и preserveAspectRatio. Это позволяет ему уместиться в области просмотра (viewport) так, как этого хотите вы, а не как это определено по умолчанию.

В большинстве случаев <symbol> подходит для определения повторно используемых элементов (символов). Он все так же служит шаблоном для <use>. А имея собственные атрибуты viewBox и preserveAspectRatio, он может растягиваться на прямоугольную область просмотра, задаваемую в ссылающемся на него элементе <use>. Учтите, что элементы <symbol> определяют новую область просмотра каждый раз, когда вызываются элементом <use>.

Это прекрасная особенность элемента <symbol>. Она позволяет определять элементы, не зависящие от области просмотра, в которую они попадут. Они будут всегда отображаться заданным образом.

Но для понимания всей этой магии вы должны знать, как вообще работают атрибуты viewBox и preserveAspectRatio. По этой теме у Chris Coyier есть статья, объясняющая, чем так хорош элемент <symbol>, почему он является хорошим выбором для реализации иконок и как в целом его использовать.

Мною уже написана обширная статья про viewport, viewBox и preserveAspectRatio. Можете с ней ознакомиться: Understanding SVG Coordinate Systems and Transformations (Part 1) – The viewport, viewBox, and preserveAspectRatio.

Стоит учесть, что свойство display не применяется к элементу <symbol>. Даже если установить его отличным от display="none", элемент все равно не будет отображаться. И в то же на время элемент <symbol> можно ссылаться, даже если у него или его потомков установлено свойство display="none".

Заключение


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

Надеюсь, вам понравилась статья, и вы нашли ее полезной. Спасибо за чтение!

От переводчика


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

С предложениями, пожеланиями и замечаниями, как обычно, в ЛС.
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 14
  • +5
    Эх, эту бы статью, да две недели назад!
    Пишу сейчас приложение для веба (внутри взаимодействие с кучей разных объектов), убил полтора месяца, пытаясь сделать его на Canvas. Сдали нервы, попробовал первый раз в жизни SVG и, о чудо, тот же функционал переписал за пару дней. В итоге сделал вывод: canvas для рисования, svg для взаимодействия между кучей различных объектов сложной формы (да-да, я знаю, что голову нужно включать заранее, но что делать, если с canvas я работал, а про SVG только слышал?).
    • +1
      А какую библиотеку использовали? Мне как раз надо будет кое что на svg сделать.
      • +1
        От себя могу посоветовать глянуть в сторону http://raphaeljs.com/.
        • +1
          Можно глянуть ещё в сторону d3.js, там очень мощные средства работы с SVG (да и вообще с любыми данными). Бонус, теперь по этой библиотеке есть русскоязычная справка :)
          • +1
            Rafael пробовал, но там слишком много всего, что мне не надо, остановился на svg.js
            • +1
          • +1
            Заглянул в исходник первой иллюстрации (с сайта автора). Там такой трэш! Никаких «head» и «wing», никакой структуры и куча пустых «g». В примере с деревьями ниаких defs и use. Чему она учит?
            • +1
              Я не сразу заметил даже, что там svg. Еще думал, почему ж на habrastorage не перезалилось.

              Взгляните внимательнее на исходник. Это изображение в Adobe Illustrator нарисовано. Отсюда и грязный код. Так что первую часть статьи можно рассматривать и как небольшой мануал по прихорашиванию автоматически генерируемого кода.

              Зато стало яснее, откуда взялись path вместо circle.
              • +1
                Там не только SVG, там еще и JPEG местами. :)

                Интересно, какой-нибудь редактор умеет правильно сохранять эти g, use, defs?
                • +1
                  Я думаю, количество графических редакторов с такой фичей примерно равно количеству WISIWYG текстовых процессоров, умеющих сохранять нормальный HTML.
                  • 0
                    Я привык визуализировать в голове, поэтому для меня MarkitUp! — лучший встраиваемый редактор.

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

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

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

                    Описанные в посте элементы, на мой взгляд, нужны, скорее, не художникам, а программистам. Для процедурной генерации изображений, например. Или для использования SVG-файла как хранилища иконок.
                    • +1
                      Согласен, художникам, в общем, ни к чему это все.
            • +1
              По невыясненным причинам автором описаны круги путями вместо использования circle

              Так код SVG сейчас пишется инкскейпом, например :)
              • 0
                Да, чуть выше уже выяснили, что это Illustrator нагенерировал.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.