Полезные рецепты ручного создания SVG
- пятница, 25 октября 2024 г. в 00:00:09
Признаюсь, поначалу я была скептиком ручного кодирования SVG. Будучи фронтенд-разработчиком, привыкшим приводить в порядок "плохие" SVG-файлы, я никогда всерьез не задумывалась о том, чтобы самой рисовать с помощью кода.
Однако, прошлой зимой я создавала проект для рисования каллиграфических сеток и с головой погрузилась в изучение спецификаций SVG. Оказалось, что, несмотря на знание базовых структур и правил работы с SVG, по-настоящему понять значение всех этих цифр и их взаимодействие между собой можно только через практику ручного кодирования.
А как только освоишься, это занятие становится на редкость увлекательным и даже забавным.
В этой статье мы не будем углубляться в сложные элементы SVG, такие как path. Наша цель — рассмотреть практические аспекты работы с простыми SVG. Когда дело доходит до рисования кривых (curves), я рекомендую использовать инструменты вроде Illustrator или Affinity. Однако, если вы захотите поэкспериментировать с составными линиями, знание path может пригодиться. Возможно, мы вернемся к этой теме в одной из следующих статей.
Основное внимание в этом руководстве уделено практическим примерам, которые помогут понять некоторые математические аспекты создания SVG-графики. Для более глубокого изучения спецификаций SVG советую обратиться к статье "A Practical Guide To SVG And Design Tools".
Illustrator, Affinity и другие программы для работы с векторной графикой в основном помогают рисовать в системе координат, а затем сохраняют эти пути и фигуры в файлах SVG.
Если открыть эти файлы в редакторе, можно увидеть, что они представляют собой набор путей, содержащих множество чисел, которые являются координатами в той системе, из которой состоят линии.
Однако, помимо всемогущего элемента <path>
, SVG предоставляет и более специализированные графические примитивы: <rect>
, <circle>
, <line>
, <ellipse>
, <polygon>
и <polyline>
.
Эти базовые элементы не так сложно создавать вручную, и они открывают много возможностей для добавления анимации и других интерактивных эффектов. Таким образом, SVG — это не просто масштабируемые растровые изображения, но и довольно полноценные фрагменты кода.
unit != unit
Прежде чем мы углубимся в технические детали отрисовки элементов SVG, стоит разобраться в особенностях работы с единицами измерения в этом формате.
Главное свойство SVG — это векторный формат, позволяющий ему быть независимым от размеров экрана. Единицы измерения в SVG отвязаны от физических характеристик устройства и вместо этого относятся к внутренней координатной системе документа.
Проще говоря, при работе с SVG мы не оперируем пикселями или другими фиксированными единицами, а используем условные числовые значения, а уже потом определяем размер документа.
Таким образом, хотя width
и height
SVG-изображения могут задаваться с помощью привычных CSS-единиц, например rem
, внутри атрибута viewBox
эти величины становятся абстрактными числами. Другими словами, единицы измерения в viewBox
служат лишь для определения относительных пропорций и не привязаны к конкретным физическим размерам.
viewBox
?viewBox
работает примерно так же, как свойство aspect-ratio
в CSS. Он помогает установить соотношение между шириной и высотой системы координат и задает границы рабочей области. Я обычно рассматриваю viewBox
как "размер документа".
Любой элемент, размещенный внутри SVG, габариты которого превышают размеры viewBox
, будет обрезаться. Таким образом, viewBox
— это срез системы координат, который мы видим на экране. При наличии атрибута viewBox
, атрибуты width
и height
становятся необязательными.
Другими словами, наличие viewBox
делает поведение SVG очень похожим на обычное изображение. И как и с изображениями, обычно удобнее задать либо width
, либо height
, позволив второму размеру подстроиться автоматически с сохранением исходных пропорций.
Итак, при создании функции для отрисовки SVG нам потребовалось бы хранить три отдельных переменных и заполнять их следующим образом:
`<svg
width="${svgWidth}"
viewBox="0 0 ${documentWidth} ${documentHeight}"
xmlns="http://www.w3.org/2000/svg"
>`;
Возможности SVG гораздо шире, чем может показаться на первый взгляд. С помощью тега use
можно превратить часто используемое изображение в symbol
; можно объединять SVG-элементы в спрайты (sprites), а также применять специальные рекомендации по использованию SVG в качестве иконок.
Однако, детальное рассмотрение этих аспектов выходит за рамки данной статьи. Здесь мы сосредоточимся в первую очередь на создании SVG-файлов, а не на их использовании и оптимизации.
Тем не менее, есть одна важная тема, которую стоит затронуть — доступность SVG.
SVG-изображения можно использовать внутри тега <img>
, где доступен атрибут alt
. Однако, такой подход ограничивает возможности взаимодействия с SVG-кодом. Поэтому во многих случаях лучшим вариантом будет именно встраивание SVG напрямую в HTML-код страницы.
Достаточно просто добавить атрибут role="img"
к SVG-элементу, а затем добавить тег <title>
с названием изображения.
Примечание: более подробные рекомендации по обеспечению доступности SVG-изображений можно найти в этой статье.
<svg
role="img"
[...attr]
>
<title>Доступный заголовок</title>
<!-- Код изображения -->
</svg>
При работе с SVG часто приходится прибегать к математическим расчетам, порой достаточно простым, а иногда и довольно сложным (например, при построении каллиграфических сеток нужно использовать тригонометрию). Но, несмотря на простую математику, многие предпочитают не писать SVG-код на чистом HTML, а хотят использовать алгебру.
Лично мне гораздо проще понимать SVG-код, когда я могу давать переменным осмысленные названия, нежели оперировать набором цифр. Поэтому в своей практике я всегда использую JS.
Таким образом, в последующих примерах мы рассмотрим необходимые переменные и простые математические выражения, а затем, как можно использовать шаблонные строки (template literals) (в стиле JSX) для более читабельной интерполяции.
Чтобы сделать это руководство независимым от конкретных фреймворков, я хочу вкратце рассмотреть создание SVG-элементов с помощью чистого JS.
Мы начнем с создания контейнерного элемента HTML, в который будет помещен SVG, и получим ссылку на этот элемент через JS.
<div data-svg-container></div>
<script src="template.js"></script>
Для простоты примера мы нарисуем прямоугольник <rect>
с заливкой, занимающий весь viewBox
.
Примечание: для заливки можно применять любые допустимые в CSS значения, от фиксированных цветов до таких динамических значений, как currentColor
или CSS-переменные. Это удобно, если SVG встраивается непосредственно в HTML-страницу и должен наследовать ее стилевое оформление.
Определим необходимые переменные:
// Переменные
const container = document.querySelector("[data-svg-container]");
const svgWidth = "30rem"; // используем любое значение с единицами измерения
const documentWidth = 100;
const documentHeight = 100;
const rectWidth = documentWidth;
const rectHeight = documentHeight;
const rectFill = "currentColor"; // используем любое допустимое значение цвета
const title = "A simple square box";
Данный метод проще в плане обеспечения типобезопасности (в случае использования TypeScript) — он задействует корректные элементы, атрибуты SVG и т.д. Однако, он менее производителен и может потребовать значительно больше времени, если речь идет о большом количестве элементов.
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
const titleElement = document.createElementNS("http://www.w3.org/2000/svg", "title");
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
svg.setAttribute("width", svgWidth);
svg.setAttribute("viewBox", `0 0 ${documentWidth} ${documentHeight}`);
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
svg.setAttribute("role", "img");
titleElement.textContent = title;
rect.setAttribute("width", rectWidth);
rect.setAttribute("height", rectHeight);
rect.setAttribute("fill", rectFill);
svg.appendChild(titleElement);
svg.appendChild(rect);
container.appendChild(svg);
В качестве альтернативы, можно создать строку SVG и присвоить ее свойству innerHTML
соответствующего контейнера. Такой вариант более производительный, но при этом теряется типобезопасность, а элементы не будут должным образом создаваться в DOM.
container.innerHTML = `
<svg
width="${svgWidth}"
viewBox="0 0 ${documentWidth} ${documentHeight}"
xmlns="http://www.w3.org/2000/svg"
role="img"
>
<title>${title}</title>
<rect
width="${rectWidth}"
height="${rectHeight}"
fill="${rectFill}"
/>
</svg>`;
Наиболее оптимальным решением является непосредственное создание SVG-элемента в качестве DOM-элемента, а затем присвоение его содержимого через свойство innerHTML
.
Таким образом, мы добавляем корректный SVG-элемент в контейнер и можем работать с ним, сохраняя типобезопасность. Поскольку содержимое SVG, как правило, не требует частого обновления, этот подход кажется наиболее предпочтительным.
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("width", svgWidth);
svg.setAttribute("viewBox", `0 0 ${documentWidth} ${documentHeight}`);
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
svg.setAttribute("role", "img");
svg.innerHTML = `
<title>${title}</title>
<rect
width="${rectWidth}"
height="${rectHeight}"
fill="${rectFill}"
/>
`;
container.appendChild(svg);
Теперь, когда мы разобрались с основами настройки SVG, самое время перейти к практике и изучить, как создавать наиболее распространенные графические элементы.
Как мы уже выяснили в предыдущем примере, элемент <rect>
служит для рисования прямоугольника. Он имеет атрибуты x
и y
, определяющие положение его верхнего левого угла. Эти атрибуты необязательны, и если они не заданы, прямоугольник будет отображаться в начале координат (0, 0)
, как в предыдущем примере.
Кроме того, у <rect>
есть атрибуты rx
и ry
, задающие радиусы скругления углов. Если вы укажете только rx
, то ry
автоматически получит такое же значение.
В нашем следующем примере мы нарисуем четыре разных прямоугольника, по одному в каждом квадранте:
Рассмотрим, как реализовать эти прямоугольники с помощью JS:
const rectDocWidth = 200;
const rectDocHeight = 200;
const rectFill = "currentColor";
const docOffset = 15;
const rectSize = rectDocWidth / 2 - docOffset * 2;
const roundedCornerRadius = 10;
const circleLookRadius = rectSize / 2;
const ellipticalRy = roundedCornerRadius * 2;
Чтобы отобразить эти прямоугольники в SVG, примиеняем соответствующие переменные к шаблону элемента:
<svg
width={svgWidth}
viewBox={`0 0 ${rectDocWidth} ${rectDocHeight}`}
xmlns="http://www.w3.org/2000/svg"
role="img"
>
<title>Four Rectangles of different qualities placed in each quadrant</title>
<rect
x={docOffset}
y={docOffset}
width={rectSize}
height={rectSize}
fill={rectFill}
/>
<rect
x={rectDocWidth - rectSize - docOffset}
rx={roundedCornerRadius}
y={docOffset}
width={rectSize}
height={rectSize}
fill={rectFill}
/>
<rect
x={docOffset}
rx={circleLookRadius}
y={rectDocHeight - rectSize - docOffset}
width={rectSize}
height={rectSize}
fill={rectFill}
/>
<rect
x={rectDocWidth - rectSize - docOffset}
rx={roundedCornerRadius}
ry={ellipticalRy}
y={rectDocHeight - rectSize - docOffset}
width={rectSize}
height={rectSize}
fill={rectFill}
/>
</svg>
В SVG доступен элемент <line>
, позволяющий рисовать прямые линии. Он имеет атрибуты x1
, y1
, x2
и y2
, определяющие координаты начальной и конечной точек линии.
Умение рисовать горизонтальные и вертикальные линии — довольно важный навык. Правило простое: для горизонтальной линии должны совпадать значения y
начальной и конечной точек, а для вертикальной линии — значения x
.
Нарисуем горизонтальную и вертикальную линии, пересекающиеся в центре документа. Я намеренно использовала несколько необычные числовые значения, но, тем не менее, в результате получилось идеально центрированное SVG-изображение. В SVG можно использовать числа с плавающей запятой.
Переменные:
const lineDocWidth = 421;
const lineDocHeight = 391;
const lineStroke = "currentColor";
const lineStrokeWidth = 5;
const horizontalLineStart = 0;
const horizontalLineEnd = lineDocWidth;
const horizontalLineY = lineDocHeight / 2;
const verticalLineStart = 0;
const verticalLineEnd = lineDocHeight;
const verticalLineX = lineDocWidth / 2;
Интегрируем их в SVG-элемент:
<svg
width={svgWidth}
viewBox={`0 0 ${lineDocWidth} ${lineDocHeight}`}
xmlns="http://www.w3.org/2000/svg"
role="img"
>
<title>Horizontal and Vertical Line through the middle of the document</title>
<line
x1={horizontalLineStart}
x2={horizontalLineEnd}
y1={horizontalLineY}
y2={horizontalLineY}
stroke={lineStroke}
stroke-width={lineStrokeWidth}
/>
<line
x1={verticalLineX}
x2={verticalLineX}
y1={verticalLineStart}
y2={verticalLineEnd}
stroke={lineStroke}
stroke-width={lineStrokeWidth}
/>
</svg>
У элемента <circle>
в SVG есть атрибуты cx
, cy
и r
, которые определяют координаты центра круга и его радиус.
Иногда возникают ситуации, когда нам необходимо, чтобы в определенной точке располагался край круга, а не его центр. Кроме того, мы зачастую мыслим в терминах диаметра, а не радиуса. В таких случаях нам придется выполнить некоторые вычисления, чтобы определить нужные координаты.
Предположим, мы хотим нарисовать круг, край которого смещен от нижнего левого угла на определенное расстояние, а его диаметр имеет заданный размер. Для этого нам понадобятся следующие переменные:
const circleDocWidth = 100;
const circleDocHeight = 100;
const circleOffset = 10;
const circleDiameter = 20;
const circleRadius = circleDiameter / 2;
const circleX = circleOffset + circleRadius;
const circleY = circleDocHeight - circleOffset - circleRadius;
Теперь так же, как и в случае с линиями, передадим эти переменные в SVG-элемент:
<svg
width={svgWidth}
viewBox={`0 0 ${circleDocWidth} ${circleDocHeight}`}
xmlns="http://www.w3.org/2000/svg"
role="img"
>
<circle
cx={circleX}
cy={circleY}
r={circleRadius}
fill="red"
/>
</svg>
У элемента <ellipse>
в SVG есть атрибуты cx
, cy
, rx
и ry
, которые определяют координаты центра эллипса, а также его горизонтальный и вертикальный радиусы.
Значения x
и y
задают положение центра эллипса, а rx
и ry
описывают его радиусы по горизонтали и вертикали, соответственно.
Представим, что нам необходимо нарисовать эллипс, смещенный от верхнего правого угла на определенное расстояние, с горизонтальным радиусом заданного размера и вертикальным радиусом, равным половине горизонтального.
Для этого определим следующие переменные:
const ellipseSVGWidth = 100;
const ellipseDocWidth = 100;
const ellipseDocHeight = 100;
const ellipseOffset = 10;
const ellipseHorizontalRadius = ellipseDocWidth / 2 - ellipseOffset;
const ellipseVerticalRadius = ellipseHorizontalRadius / 2;
const ellipseX = ellipseDocWidth - ellipseOffset - ellipseHorizontalRadius;
const ellipseY = ellipseOffset + ellipseVerticalRadius;
Интегрируем в SVG-элемент:
<svg
width={svgWidth}
viewBox={`0 0 ${ellipseDocWidth} ${ellipseDocHeight}`}
xmlns="http://www.w3.org/2000/svg"
role="img"
>
<title>Ellipse offset from the top right corner</title>
<ellipse
cx={ellipseX}
cy={ellipseY}
rx={ellipseHorizontalRadius}
ry={ellipseVerticalRadius}
fill="hotpink"
/>
</svg>
<polyline>
и <polygon>
Иногда нам требуется создать линию, состоящую из нескольких точек, но не образующую прямоугольник или круг. В таких случаях можно использовать элементы polyline
и polygon
, которые имеют одинаковые атрибуты, но отличаются способом соединения первой и последней точек: polygon
замыкает фигуру, а polyline
— нет.
Оба элемента применяют атрибут points
, содержащий список координат x
и y
, разделенных пробелами. По умолчанию у них установлена fill
(заливка), что не всегда уместно, особенно для polyline
, поэтому часто приходится задавать ей значение none
.
Допустим, у нас есть три круга, и мы хотим соединить их центры линиями. Для этого достаточно взять координаты cx
и cy
этих кругов и объединить их в атрибуте points
.
Важно помнить, что SVG рисуется от заднего плана к переднему, поэтому сначала будут нарисованы круги, а затем — линии, которые окажутся сверху кругов.
Чтобы наглядно продемонстрировать различия между polyline
и polygon
, мы нарисуем композицию четыре раза, как уже делали с кругами.
В этот раз у нас будет больше одного элемента. Чтобы быстрее ориентироваться, какие элементы относятся к одной группе, мы можем использовать элемент g
, объединяющий несколько элементов. Это позволяет применять определенные атрибуты ко всем дочерним элементам одновременно.
Чтобы сэкономить время на настройке положения каждого отдельного элемента композиции, мы можем применить transform
к группе, чтобы разместить ее в разных квадрантах.
Для этого используется атрибут transform="translate(x,y)"
, работающий аналогично CSS-трансформациям, с небольшими отличиями в синтаксисе. В большинстве простых случаев мы можем предполагать, что он работает так же. Атрибут translate
перемещает содержимое группы вдоль осей x
и y
.
Посмотрим на SVG:
Слева направо, снизу вверх: polyline без заливки, polyline с заливкой, polygon без заливки, polygon с заливкой
Здесь мы видим, что при использовании одних и тех же координат, polyline
не соединяет линией синюю и красную точки, в отличие от polygon
. Но при применении заливки, они оба воспринимают форму как замкнутую, что видно на правом верхнем квадранте.
Это уже второй случай, когда мы имеем дело с определенной долей повторения, поэтому стоит рассмотреть, как можно использовать возможности JS для более эффективного создания шаблона.
Но сначала давайте реализуем базовый вариант, как мы делали ранее. Мы создадим объекты для кругов, а затем объединим значения cx
и cy
, чтобы создать атрибут points
. Также сохраним наши трансформации в переменных.
const polyDocWidth = 200;
const polyDocHeight = 200;
const circleOne = { cx: 25, cy: 80, r: 10, fill: "red" };
const circleTwo = { cx: 40, cy: 20, r: 5, fill: "lime" };
const circleThree = { cx: 70, cy: 60, r: 8, fill: "cyan" };
const points = `${circleOne.cx},${circleOne.cy} ${circleTwo.cx},${circleTwo.cy} ${circleThree.cx},${circleThree.cy}`;
const moveToTopRight = `translate(${polyDocWidth / 2}, 0)`;
const moveToBottomRight = `translate(${polyDocWidth / 2}, ${polyDocHeight / 2})`;
const moveToBottomLeft = `translate(0, ${polyDocHeight / 2})`;
Далее, применяем эти переменные к шаблону, используя либо polyline
, либо polygon
, а также атрибут fill
с цветовым значением или none
.
<svg
width={svgWidth}
viewBox={`0 0 ${polyDocWidth} ${polyDocHeight}`}
xmlns="http://www.w3.org/2000/svg"
role="img"
>
<title>Composite shape comparison</title>
<g>
<circle
cx={circleOne.cx}
cy={circleOne.cy}
r={circleOne.r}
fill={circleOne.fill}
/>
<circle
cx={circleTwo.cx}
cy={circleTwo.cy}
r={circleTwo.r}
fill={circleTwo.fill}
/>
<circle
cx={circleThree.cx}
cy={circleThree.cy}
r={circleThree.r}
fill={circleThree.fill}
/>
<polyline
points={points}
fill="none"
stroke="black"
/>
</g>
<g transform={moveToTopRight}>
<circle
cx={circleOne.cx}
cy={circleOne.cy}
r={circleOne.r}
fill={circleOne.fill}
/>
<circle
cx={circleTwo.cx}
cy={circleTwo.cy}
r={circleTwo.r}
fill={circleTwo.fill}
/>
<circle
cx={circleThree.cx}
cy={circleThree.cy}
r={circleThree.r}
fill={circleThree.fill}
/>
<polyline
points={points}
fill="white"
stroke="black"
/>
</g>
<g transform={moveToBottomLeft}>
<circle
cx={circleOne.cx}
cy={circleOne.cy}
r={circleOne.r}
fill={circleOne.fill}
/>
<circle
cx={circleTwo.cx}
cy={circleTwo.cy}
r={circleTwo.r}
fill={circleTwo.fill}
/>
<circle
cx={circleThree.cx}
cy={circleThree.cy}
r={circleThree.r}
fill={circleThree.fill}
/>
<polygon
points={points}
fill="none"
stroke="black"
/>
</g>
<g transform={moveToBottomRight}>
<circle
cx={circleOne.cx}
cy={circleOne.cy}
r={circleOne.r}
fill={circleOne.fill}
/>
<circle
cx={circleTwo.cx}
cy={circleTwo.cy}
r={circleTwo.r}
fill={circleTwo.fill}
/>
<circle
cx={circleThree.cx}
cy={circleThree.cy}
r={circleThree.r}
fill={circleThree.fill}
/>
<polygon
points={points}
fill="white"
stroke="black"
/>
</g>
</svg>
При создании SVG-графики можно столкнуться с необходимостью многократного повторения одного и того же кода. В этих случаях JS может стать надежным помощником. Давайте еще раз рассмотрим предыдущий пример и оптимизируем его, сократив количество повторений.
Наблюдения:
circle
, следующие одному и тому же шаблонуpolyline
, либо polygon
Вывод: мы можем применить вложенные циклы.
Давайте вернемся к простой реализации, так как способы использования циклов существенно различаются между разными фреймворками.
Можно было бы сделать этот подход более универсальным, написав отдельные генераторные функции для каждого типа элемента. Но сейчас мы лишь рассмотрим общую идею того, что можно сделать с точки зрения логики. Безусловно, существуют и другие способы оптимизации.
Я решила использовать массивы для каждого типа, а затем написала вспомогательную функцию, которая перебирает данные и формирует массив объектов со всей необходимой информацией для каждой группы. При таком небольшом объеме данных вполне можно было бы хранить все в одном элементе, где значения повторяются, но в данном случае мы серьезно относимся к принципу DRY (Don't Repeat Yourself).
const container = document.querySelector("[data-svg-container]");
const svgWidth = 200;
const documentWidth = 200;
const documentHeight = 200;
const halfWidth = documentWidth / 2;
const halfHeight = documentHeight / 2;
const circles = [
{ cx: 25, cy: 80, r: 10, fill: "red" },
{ cx: 40, cy: 20, r: 5, fill: "lime" },
{ cx: 70, cy: 60, r: 8, fill: "cyan" },
];
const points = circles.map(({ cx, cy }) => `${cx},${cy}`).join(" ");
const elements = ["polyline", "polygon"];
const fillOptions = ["none", "white"];
const transforms = [
undefined,
`translate(${halfWidth}, 0)`,
`translate(0, ${halfHeight})`,
`translate(${halfWidth}, ${halfHeight})`,
];
const makeGroupsDataObject = () => {
let counter = 0;
const g = [];
elements.forEach((element) => {
fillOptions.forEach((fill) => {
const transform = transforms[counter++];
g.push({ element, fill, transform });
});
});
return g;
};
const groups = makeGroupsDataObject();
// result:
// [
// {
// element: "polyline",
// fill: "none",
// },
// {
// element: "polyline",
// fill: "white",
// transform: "translate(100, 0)",
// },
// {
// element: "polygon",
// fill: "none",
// transform: "translate(0, 100)",
// },
// {
// element: "polygon",
// fill: "white",
// transform: "translate(100, 100)",
// }
// ]
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("width", svgWidth);
svg.setAttribute("viewBox", `0 0 ${documentWidth} ${documentHeight}`);
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
svg.setAttribute("role", "img");
svg.innerHTML = "<title>Composite shape comparison</title>";
groups.forEach((groupData) => {
const circlesHTML = circles
.map((circle) => {
return `
<circle
cx="${circle.cx}"
cy="${circle.cy}"
r="${circle.r}"
fill="${circle.fill}"
/>`;
})
.join("");
const polyElementHTML = `
<${groupData.element}
points="${points}"
fill="${groupData.fill}"
stroke="black"
/>`;
const group = `
<g ${groupData.transform ? `transform="${groupData.transform}"` : ""}>
${circlesHTML}
${polyElementHTML}
</g>
`;
svg.innerHTML += group;
});
container.appendChild(svg);
Мы рассмотрели базовые аспекты работы с SVG, но на самом деле возможности этого формата гораздо шире. Помимо трансформаций (transform
), о которых мы говорили, можно применять маски (mask
), маркеры (marker
) и многое другое.
К сожалению, мы не сможем погрузиться во все эти интересные темы прямо сейчас. Но поскольку наше знакомство с SVG началось с создания сеток для каллиграфии, я бы хотела рассказать вам о двух самых увлекательных фишках, которые, к сожалению, я не могу использовать в своем генераторе. Дело в том, что я стремилась сделать так, чтобы сгенерированные SVG-файлы можно было открывать в Affinity, а эта программа не поддерживает шаблоны (pattern
).
Итак, шаблоны являются частью раздела defs
в SVG. Здесь можно определять многоразовые элементы, на которые затем можно ссылаться в SVG.
pattern
Если подумать, график представляет собой набор горизонтальных и вертикальных линий, повторяющихся вдоль осей X и Y.
В этом случае отличным решением будет использование шаблонов в SVG. Мы можем создать <rect>
и затем указать шаблон в его атрибуте fill
. Сам шаблон будет иметь свои width
, height
и viewBox
, которые определяют, как он будет повторяться.
Предположим, мы хотим разместить сетку графика по центру, при этом иметь возможность задавать размеры квадратных ячеек.
Для этого нам понадобится несколько переменных:
const graphDocWidth = 226;
const graphDocHeight = 101;
const cellSize = 5;
const strokeWidth = 0.3;
const strokeColor = "currentColor";
const patternHeight = (cellSize / graphDocHeight) * 100;
const patternWidth = (cellSize / graphDocWidth) * 100;
const gridYStart = (graphDocHeight % cellSize) / 2;
const gridXStart = (graphDocWidth % cellSize) / 2;
Интегрируем их в SVG-элемент:
<svg
width={svgWidth}
viewBox={`0 0 ${graphDocWidth} ${graphDocHeight}`}
xmlns="http://www.w3.org/2000/svg"
role="img"
>
<defs>
<pattern
id="horizontal"
viewBox={`0 0 ${graphDocWidth} ${strokeWidth}`}
width="100%"
height={`${patternHeight}%`}
>
<line
x1="0"
x2={graphDocWidth}
y1={gridYStart}
y2={gridYStart}
stroke={strokeColor}
stroke-width={strokeWidth}
/>
</pattern>
<pattern
id="vertical"
viewBox={`0 0 ${strokeWidth} ${graphDocHeight}`}
width={`${patternWidth}%`}
height="100%"
>
<line
y1={0}
y2={graphDocHeight}
x1={gridXStart}
x2={gridXStart}
stroke={strokeColor}
stroke-width={strokeWidth}
/>
</pattern>
</defs>
<title>A graph grid</title>
<rect
width={graphDocWidth}
height={graphDocHeight}
fill="url(#horizontal)"
/>
<rect
width={graphDocWidth}
height={graphDocHeight}
fill="url(#vertical)"
/>
</svg>
pattern
Если нам нужна не линейная, а точечная сетка, вместо повторяющихся линий мы можем использовать окружности. Или, как альтернативный вариант, пунктирные линии, созданные с помощью свойств stroke-dasharray
и stroke-dashoffset
. В последнем случае нам понадобится всего одна линия.
const dotDocWidth = 219;
const dotDocHeight = 100;
const cellSize = 4;
const strokeColor = "black";
const gridYStart = (dotDocHeight % cellSize) / 2;
const gridXStart = (dotDocWidth % cellSize) / 2;
const dotSize = 0.5;
const patternHeight = (cellSize / dotDocHeight) * 100;
Добавляем переменные в SVG-элемент:
<svg
width={svgWidth}
viewBox={`0 0 ${dotDocWidth} ${dotDocHeight}`}
xmlns="http://www.w3.org/2000/svg"
role="img"
>
<defs>
<pattern
id="horizontal-dotted-line"
viewBox={`0 0 ${dotDocWidth} ${dotSize}`}
width="100%"
height={`${patternHeight}%`}
>
<line
x1={gridXStart}
y1={gridYStart}
x2={dotDocWidth}
y2={gridYStart}
stroke={strokeColor}
stroke-width={dotSize}
stroke-dasharray={`0,${cellSize}`}
stroke-linecap="round"
></line>
</pattern>
</defs>
<title>A Dot Grid</title>
<rect
x="0"
y="0"
width={dotDocWidth}
height={dotDocHeight}
fill="url(#horizontal-dotted-line)"
></rect>
</svg>
На этом мы завершаем наше небольшое вводное путешествие в мир SVG. Как видите, создание SVG вручную не такое страшное, как может показаться на первый взгляд. Разбив задачу на отдельные шаги, мы понимаем, что это не сильно отличается от других задач программирования:
Надеюсь, данная статья послужила для вас отправной точкой в исследовании удивительного мира кодируемой графики. Теперь у вас есть мотивация углубиться в спецификации и попробовать создать что-нибудь самостоятельно. Пусть этот опыт вдохновит вас на новые творческие эксперименты!
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩