javascript

Полезные рецепты ручного создания SVG

  • пятница, 25 октября 2024 г. в 00:00:09
https://habr.com/ru/companies/timeweb/articles/852522/



Признаюсь, поначалу я была скептиком ручного кодирования 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 — это не просто масштабируемые растровые изображения, но и довольно полноценные фрагменты кода.


Как работает 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


Возможности 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 с помощью JavaScript


При работе с 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";

Метод 1. Создание элемента и установка атрибутов


Данный метод проще в плане обеспечения типобезопасности (в случае использования 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);


Метод 2. Создание 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>`;


Метод 3. Берем лучшее от двух вариантов


Наиболее оптимальным решением является непосредственное создание 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 автоматически получит такое же значение.


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


  1. Верхний левый: это обычный прямоугольник со смещением по верхнему и левому краям и заданными шириной и высотой.
  2. Верхний правый: здесь мы применяем небольшой радиус скругления углов, чтобы получить закругленный прямоугольник.
  3. Нижний левый: в этом случае радиус скругления настолько велик, что прямоугольник превращается в круг. При этом его начало координат немного смещено.
  4. Нижний правый: здесь используется эллиптический радиус скругления для создания сглаженной квадратной формы.

Рассмотрим, как реализовать эти прямоугольники с помощью 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


Мы рассмотрели базовые аспекты работы с 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 вручную не такое страшное, как может показаться на первый взгляд. Разбив задачу на отдельные шаги, мы понимаем, что это не сильно отличается от других задач программирования:


  1. Мы изучаем проблему.
  2. Разбиваем ее на более мелкие части.
  3. Исследуем каждую координату и ее математическое представление.
  4. А затем собираем все воедино.

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




Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале