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

  1. Tools
  2. Overview
  3. HTML & SCSS
  4. Javascript
  5. Resources

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:

  1. Find the svg that has the same id as the href that was clicked.
  2. Get the values of each points attribute in the polygons in that svg and put them into an object. Add this object to the to array.
  3. Animate the from values to the to values.
  4. After animation, set the from array to the to 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
    });
  });

Resources

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

11,144 8 163