Анимация SVG-полигонов

monochromer
5 min readOct 11, 2017

--

Перевод статьи Nataliya Sayenko — Animating SVG polygons. Опубликовано с разрешения автора.

Введение

Я получила много вопросов о том, как был создан код из примера “animating svg polygon points”, поэтому я решила написать небольшое руководство. Я создала этот пример, изучая, как устроен сайт facesofpower.net.

Оговорка

Я не знаю, сделала ли я это лучшим или оптимальным способом, так что если кто-либо имеет какие-нибудь советы или замечания, дайте мне знать!

Содержание

  1. Инструменты
  2. Обзор
  3. HTML и SCSS
  4. JavaScript (Переменные и Функции)
  5. Ссылки

Инструменты

Я использовала primitive, который преобразует изображения в SVG. Я использовала консольную команду:

primitive -i input.jpg -o output.svg -n 250 -m 1

-n 250 задает 250 многоугольников, -m 1 задает для них треугольную форму, а -i input.jpg -o output.svg определяет входной и выходной файл.

Важно указать именно треугольники для нашего примера, потому что это повлияет на то, как будет работать атрибут points для полигонов. Если вы укажете другую форму, вам необходимо будет обновить регулярное выражение в функции getCoordinates и метод setAttribute в функции animatePolygons соответственно.

Анимации созданы с помощью TweenMax.

Общий алгоритм

Мы будем анимировать атрибут points для каждого полигона. Мы собираемся сделать это, имея два массива: один со начальными значениями анимации, другой — с конечными. Массив с начальными значениями всегда будет соответствовать svg-элементу с классом svg-holder, так как он всегда должен отображаться на странице.

Каждый раз, когда мы кликаем по ссылке:

  • Находим svg-элемент с таким же id, как и атрибут href у ссылки
  • Получем значения каждого атрибута points в многоугольниках в наденном svg и помещаем их в объект. Добавляем этот объект в массив to.
  • Анимируем значения из массива from в значения из массива to.
  • После анимации массиву to присваиваем массив from.

HTML и SCSS

После создания элементов SVG, поместите их в внутрь тега body html-документа. Продублируйте первый svg-элемент и задайте ему класс svg-holder. Из всех svg видимым будет только svg-holder и он будет контейнером для всех анимируемых полигонов.

Задайте всем svg-элементам, кроме svg-holder, класс hidden и уникальный id. Этот id должен совпадать с атрибутом href у ссылок. Элементы с классом hidden будут скрыты с помощью display: none;

Важно убедиться, что атрибут href каждой ссылки совпадает с id соответствующего svg.

<a href="#nat">Nat</a>
<a href="#bwl">bwl</a>
<a href="#kevin">kevin</a>
<svg class="svg-holder">
polygons for #nat go here
</svg>
<svg id="nat" class="hidden">
polygons for #nat go here
</svg>
<svg id="bwl" class="hidden">
polygons for #bwl go here
</svg>
<svg id="kevin" class="hidden">
polygons for #kevin go here
</svg>

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

JavaScript

Переменные

Сначала объявим наши переменные:

let toPolygonArray = [];
let fromPolygonArray = [];
const links = document.querySelectorAll("a");

Важными здесь являются первые два массива. Они объявляются с использованием let, потому что массивы не будут иметь постоянного значения. Массивы fromPolygonArray и toPolygonArray будут содержать начальные и конечные значения анимации соответственно.

У ссылок будет обработчик клика, который запустит анимацию.

Функции

Обработчик клика

[].forEach.call(links, function(el, i, els) {
el.addEventListener("click", function(event) {
const idToAnimateTo = this.getAttribute("href").substring(1);

[].forEach.call(els, function(el) {
if (el !== this) {
el.classList.remove("active");
} else {
this.classList.add("active");
}
}, this);

event.preventDefault();
this.classList.add("active");
updatePolygonArrays(idToAnimateTo);
});
});

Каждый раз, когда происходит клик по ссылке, эта функция получает значение атрибута href и устанавливает его в переменную idToAnimateTo:

const idToAnimateTo = this.getAttribute("href").substring(1);

Далее она вызывает функцию updatePolygonArrays с аргументом idToAnimateTo.

updatePolygonArrays(idToAnimateTo);

Остальная часть обработчика событий клика устанавливает активный класс на ссылке, которая была нажата. Так мы можем задать особое оформление для нее.

Функция getCoordinates

const getCoordinates = (polygon) => {
return polygon.getAttribute("points").match(/(-?[0-9][0-9\.]*),(-?[0-9][0-9\.]*)\ (-?[0-9][0-9\.]*),(-?[0-9][0-9\.]*)\ (-?[0-9][0-9\.]*),(-?[0-9][0-9\.]*)/);
};

Функция принимает элемент polygon и возвращает массив координат из его атрибута points.

Эта функция может отличаться в зависимости от установленного атрибута points. В моем примере каждый атрибут points имеет одинаковый вид: 6 чисел, разделенные запятыми или пробелами. Эти 6 чисел — это координаты вершин треугольника.

<polygon fill="#ffffff" points="-16,-16 32,69 271,7" />

Функция использует регулярное выражение для поиска каждого числа. Регулярное выражение может быть отредактировано, если вы хотите анимировать, например, атрибут d.

В примере выше элементы path выглядят так:

<path d="M46,282L28,228L62,184Z" fill="rgb(7, 7, 7)" fill-opacity="0.66" />

А соответствующим регулярным выражением для атрибута d будет:

path.getAttribute("d").match(/M(-?[0-9][0-9]*),(-?[0-9][0-9]*)L(-?[0-9][0-9]*),(-?[0-9][0-9]*)L(-?[0-9][0-9]*),(-?[0-9][0-9]*)Z/);

Функция createPolygonPointsObject

const createPolygonPointsObject = (polygons) => {
const polygonsArray = [];

polygons.forEach((polygon, i) => {
const coordinates = getCoordinates(polygon);

polygonsArray.push({
fill: polygon.getAttribute("fill"),
one: coordinates[1],
two: coordinates[2],
three: coordinates[3],
four: coordinates[4],
five: coordinates[5],
six: coordinates[6]
});
});

return polygonsArray;
}

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

Создайте пустой массив:

const polygonsArray = [];

Возьмите массив многоугольников и вызовете функцию getCoordinates для каждого элемента массива.

polygons.forEach((polygon, i) => {
const coordinates = getCoordinates(polygon);
});

Это присвоит константе coordinates массив чисел из атрибута points.

В том же цикле forEach поместите атрибут fill и кординаты вершин каждого полигона в массив polygonsArray. Эти значения помещаются как объекты, что, на мой взгляд, облегчает работу со значениями при их анимации.

Теперь polygonsArray — это массив объектов. Каждый объект соответсвует одному многоугольнику и его 7-ми свойствам: атрибут fill и 6 чисел из атрибута points. Так, например, вы можете получить доступ к атрибуту 3-го полигона с помощью polygonsArray[2].fill. А получить доступ к 1-ой координате 3-го многоугольника можно так polygonsArray[2].one.

polygons.forEach((polygon, i) => {
const coordinates = getCoordinates(polygon);

polygonsArray.push({
fill: polygon.getAttribute("fill"),
one: coordinates[1],
two: coordinates[2],
three: coordinates[3],
four: coordinates[4],
five: coordinates[5],
six: coordinates[6]
});
});

Верните массив из функции:

return polygonsArray;

Функция updatePolygonArrays

const updatePolygonArrays = (idToAnimateTo) => {
toPolygonArray = createPolygonPointsObject(document.getElementById(idToAnimateTo).querySelectorAll("polygon"));

animatePolygons();

fromPolygonArray = toPolygonArray;
}

Эта функция вызывается внутри обработчика клика. idToAnimateTo — это значение из атрибута href ссылки, которая была нажата. Таким образом, эта функция находит svg с id, соответствующим href. Она находит все полигоны в этом SVG, пропускает их через createPolygonPointsObject и помещает их в toPolygonArray.

Итак, теперь, получить доступ к атрибуту fill третьего многоугольника, который мы будем анимировать, можно с . помощью PolygonArray [2].fill.

Далее вызывается функция animatePolygons.

После анимации, fromPolygonArray обновляется, чтобы была возможность получить значения toPolygonArray предыдущего цикла.

Функция animatePolygons

const animatePolygons = () => {
const polygons = document.querySelector(".svg-holder").querySelectorAll("polygon");
fromPolygonArray = createPolygonPointsObject(polygons);

fromPolygonArray.forEach((obj, i) => {
TweenMax.to(obj, 1, {
one: toPolygonArray[i].one,
two: toPolygonArray[i].two,
three: toPolygonArray[i].three,
four: toPolygonArray[i].four,
five: toPolygonArray[i].five,
six: toPolygonArray[i].six,
ease: Power3.easeOut,
onUpdate: () => {
polygons[i].setAttribute("points", `${obj.one},${obj.two} ${obj.three},${obj.four} ${obj.five},${obj.six}`);
}
});
});

// animate color
polygons.forEach((polygon, i) => {
const toColor = toPolygonArray[i].fill;

TweenLite.to(polygon, 1, {
fill: toColor,
ease: Power3.easeOut
});
});
}

Возьмите текущие полигоны из элемента svg-holder. Это SVG, который видим в данный момент, а это полигоны, которые мы собираемся анимировать:

const polygons = document.querySelector(".svg-holder").querySelectorAll("polygon");

Пропустите их через функцию createPolygonPointsObject и поместите их в массив from :

fromPolygonArray = createPolygonPointsObject(polygons);

Анимация положения и размеров полигона

fromPolygonArray.forEach((obj, i) => {
TweenMax.to(obj, 1, {
one: toPolygonArray[i].one,
two: toPolygonArray[i].two,
three: toPolygonArray[i].three,
four: toPolygonArray[i].four,
five: toPolygonArray[i].five,
six: toPolygonArray[i].six,
})
});

Установите значения из массива from в значения массива to.

onUpdate: () => {
polygons[i].setAttribute("points", `${obj.one},${obj.two} ${obj.three},${obj.four} ${obj.five},${obj.six}`);
}

На каждом шаге анимации в атрибут points видимых в данный момент моногоугольников (.svg-holder) установите новые значения. Метод onUpdate в TweenMaX вызывается каждый раз при обновлении анимации, поэтому здесь он будет запускаться при каждом изменении значений obj.one, obj.two, obj.three и т. д. Так мы устанавливаем атрибут points для каждого значения между obj.one и toPolygonArray[i].one. В общем, это и есть анимация, так как мы меняем атрибут на каждом шаге.

Анимация заливки полигона

Для каждого полигона .svg-holder установите заливку из элемента массива toPolygonArray с тем же номером.

polygons.forEach((polygon, i) => {
const toColor = toPolygonArray[i].fill;

TweenLite.to(polygon, 1, {
fill: toColor,
ease: Power3.easeOut
});
});

Ссылки

  1. Regex
  2. SVG polygons
  3. TweenMax.to

P.S.: От пер.: я также сделал пару модифицированных примеров:

1 .https://codepen.io/monochromer/pen/gGjPKe — использует некоторые возможности GSAP для сокращения кода.

2. https://codepen.io/monochromer/pen/KXBgyJ — реализация на canvas.

3. https://codepen.io/monochromer/pen/GxrOOb — реализация на WebGL(PIXI)

--

--