Animating SVG polygons
Introduction
I got a lot of questions about how I created my pen "animating svg polygon points", so I decided to write a little tutorial. I made the pen trying to reverse engineer the website facesofpower.net.
Disclaimer
I don't know if I did this the best or optimal way, so if anyone has any advice/criticism let me know!
Table of Contents
Tools
- I used primitive, which converts images into SVGs.
The terminal command I used was
primitive -i input.jpg -o output.svg -n 250 -m 1
. The-n 250
specifies 250 polygons,-m 1
specifies a triangle shape, and-i input.jpg -o output.svg
are the input and output.
It's important to specify triangles for this specific example because this will influence how the points
attribute of the polygons will look. If you specify a different shape, you'll have to update the regex in the getCoordinates function and the setAttribute method in the animatePolygons function accordingly.
- The animations are made with TweenMax.
General Overview of the Logic
We will be animating the points
attribute of each polygon. We're going to do this by having two arrays: one with the values we're animating to, and one with the values we're animating from. The array that holds the values we're animating from will always be the one with a class of svg-holder
since that is the one that is visible on the page.
Every time a link is clicked:
- Find the svg that has the same id as the href that was clicked.
- Get the values of each
points
attribute in the polygons in that svg and put them into an object. Add this object to theto
array. - Animate the
from
values to theto
values. - After animation, set the
from
array to theto
array.
HTML & SCSS
After creating the SVGs, paste them into the body of the HTML. Duplicate the first SVG and give one of them the class svg-holder
. The svg-holder
is going to be the only one that is technically visible and it will be the holder for all the polygons that are animated in and out of it.
Give all SVGs except the holder the class hidden
and an id with a unique name. This id should match the href in the links. The hidden
SVGs will be hidden with display: none;
.
It's important to make sure the href of each link matches the id of it's respective 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>
There is also some link styling. This is how it should look at this point:
Javascript
Variables
First, lets declare our variables.
let toPolygonArray = [];
let fromPolygonArray = [];
const links = document.querySelectorAll("a");
The important ones here are the first two arrays. They are declared using let
because the arrays aren't going to have a constant value. The toPolygonArray
will hold the polygons we are going to animate to and the fromPolygonArray
will hold the polygons we are animating from.
The links will have a click event listener that will set off the animation.
Functions
Click Event Listener
[].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);
});
});
Every time a link is clicked, this function gets the href
and sets it to the idToAnimateTo
variable:
const idToAnimateTo = this.getAttribute("href").substring(1);
Then it calls the updatePolygonArrays
function with idToAnimateTo
as an argument:
updatePolygonArrays(idToAnimateTo);
The rest of the click event listener is setting an active class on the link that was just clicked so that the active link can have different styling.
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\.]*)/);
};
This function takes in a polygon
element and returns an array of all the numbers in the points
attribute.
This function can be different depending on how the points
attribute is set up. In my example, each points
has the same exact setup: 6 numbers separated by a comma or space. The 6 numbers are the x,y coordinates of the triangle.
<polygon fill="#ffffff" points="-16,-16 32,69 271,7" />
The function is using regex to find each number. The regex can be edited if you want to animate the d
attribute of a path, for example.
In the above example, the paths look like this:
<path d="M46,282L28,228L62,184Z" fill="rgb(7, 7, 7)" fill-opacity="0.66"/>
The corresponding regex for the d attribute would be:
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;
}
This function takes polygons
as an argument and returns an array with objects containing each polygon's fill and points attributes.
Create an empty array:
const polygonsArray = [];
Take the argument (an array of polygons), and call the getCoordinates
function on each item in the array.
polygons.forEach((polygon, i) => {
const coordinates = getCoordinates(polygon);
});
This sets coordinates
to an array of the numbers in points
.
In the same forEach
, push the points values and the fill
attribute of each polygon into the polygonsArray
. These values are getting pushed as objects, which I think makes it easier to deal with the values when animating them.
So now polygonsArray
is an array of objects. Each object refers to one polygon and has 7 properties: the fill attribute and the 6 numbers in the points
attribute. So to access the third polygon's fill attribute, for example, you can just do polygonsArray[2].fill
. To access the first number in the third polygon's points
attribute, you would do 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 the array.
return polygonsArray;
updatePolygonArrays
const updatePolygonArrays = (idToAnimateTo) => {
toPolygonArray = createPolygonPointsObject(document.getElementById(idToAnimateTo).querySelectorAll("polygon"));
animatePolygons();
fromPolygonArray = toPolygonArray;
}
This is the function that is called within the click event listener.
idToAnimateTo
is the value of the href of the link that was just clicked. So this function finds the svg with an id matching that href. It finds all the polygons in this SVG, runs them through createPolygonPointsObject
, and sets it to the toPolygonArray
.
So now, to access the fill of the third polygon that we are going to be animating to, it would be toPolygonArray[2].fill
.
Next, animatePolygons
is called.
After animation, the fromPolygonArray
is updated to have the previous cycle's toPolygonArray
values.
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
});
});
}
Get the current svg-holder
polygons. This is the currently visible SVG, and these are the polygons we're going to animate.
const polygons = document.querySelector(".svg-holder").querySelectorAll("polygon");
Run these polygons through createPolygonPointsObject
and set them to the from
array.
fromPolygonArray = createPolygonPointsObject(polygons);
Animate Polygon Position/Size
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,
})
});
Set the values in the from
array to the values of the to
array.
onUpdate: () => {
polygons[i].setAttribute("points", `${obj.one},${obj.two} ${obj.three},${obj.four} ${obj.five},${obj.six}`);
}
On every frame of the animation, set the points
attribute of the currently visible polygons (.svg-holder
) to the new values set above. The onUpdate
method in TweenMaX is called everytime the animation updates, so here it'll get run on every change in the values of obj.one
, obj.two
,obj.three
, and so on. So we're setting the points
attribute for every value between obj.one
and toPolygonArray[i].one
. This is basically where the animation happens, since we're changing the attribute on every step.
Animate Polygon fill
For each polygon in .svg-holder
, set the fill to the fill in the toPolygonArray
that is in the same index.
polygons.forEach((polygon, i) => {
const toColor = toPolygonArray[i].fill;
TweenLite.to(polygon, 1, {
fill: toColor,
ease: Power3.easeOut
});
});