Eliminate inefficient code with a little math and some SVG

by February 22, 2018

My development team at CA Technologies is working on building a new UI component library in React called Mineral UI. It’s an open source project that we’re really proud of. As we were building the website for the library, we wanted to put glittering gems animation in the masthead of the homepage. Our first attempt created a disastrous performance problem where we were monopolizing the JS thread and thrashing the DOM. Fortunately, with a little math and some SVG, we were able to eliminate all of that inefficient code.

Here is the story about how we were able to delete 2000 lines of inefficient JavaScript with SVG.

This gif is limited by size constraints. Head over to http://mineral-ui.com for the full effect.

We Were Cooking Laptops

We found this project where msurguy was changing the color of a triangle mesh in relation to the position of the cursor. It was a very nice interactive experience, but we didn’t want ours to be interactive. We just wanted the light to move around slowly on its own and make the triangles glitter as the light got closer or farther away from them.

We tried to take msurguy’s code and add a simple loop to update the light position every couple of milliseconds. Doing that we were able to get the effect we wanted, but we realized pretty quickly that this solution was not going to work for us long-term. Whenever we loaded the page with the animation running, our laptop fans would start spinning, our batteries would start draining rapidly, and we weren’t able to scroll the page!

On closer inspection of our inclusion of msurguy’s code, it became clear that we were hammering the JS thread. Every 15 milliseconds, we were removing ~230 triangles from the DOM, recomputing their color in relation to the updated light position, and then reinserting them with an updated color value. That’s a lot of DOM thrash!

SVG Animations To The Rescue

To overcome this problem, we decided to generate the animation we wanted in a static SVG file. SVG is great because it’s declarative and portable. However, it does not have a declarative way of expressing the animation we wanted: changing the colors of a set of triangles in relation to an arbitrary light moving over them. So we had to do some computations before we could write out an SVG file with the correct animations written into it. And before we could do those computations, we needed some data to work with.

We started by loading our website with the inefficient animation, killed the loop, and we were left with an image like this.

All the triangles with some light

From there we copied the raw SVG contents into a file. This gave us ~230 lines of XML like so:

Look at all those points! Msurguy has a mesh function in his project that distributes triangles over a canvas nicely that we didn’t want to have to rewrite ourselves. We simply wanted to take the output of that mesh function and then render those triangles ourselves, so we turned that XML into a JS object.

Now that we’d scrubbed our triangle data, we wanted to make sure that they could all be seen. The SVG viewBox needs an xminyminwidth and height, and if you don’t get them right, you can inadvertently crop your image. So we wrote a compute.dimensions function to read the data and return those values. We also wrote a draw.triangle function to return the SVG tags representing a triangle.

When we took a look at gems.svg we could see all the triangles!

Having found the proper viewBox, we wanted to define where our light to begin and end. We expressed the startand end location as an x and ycoordinate.

To render that, we composed a call to draw.line with draw.triangle like so.

And just like that we were able to see what we were shooting for.

Our triangles with a red lightPath

In addition to the start and end coordinates, we also needed to express the times at which we wanted the animation to start and stop. Since our lightPath already had the location information, we decided to add the time information there too.

An important part of the effect that we were after, is that the triangles closer to the light should be brighter than triangles that are farther away. Some triangles outside of a certain radius should not be illuminated at all. In order to achieve this effect, we needed to define a radius for our light so that we could illuminate the triangles within the light circle and leave the others alone.

Here we’ve added the times to the start and end and the radius to the description of the lightPath.

To figure out what to do next, we wanted to reduce the complexity a bit and just focus on a single triangle. Since each triangle in our list has a distinct id, we could apply a filter to select the perfect triangle. Until now, we’d been rendering our triangles by mapping them through the draw.triangle function. It would take twice as long to add a filter function before the map, so instead, we’ll use a reduce to do the filtering and the rendering in a single pass.

We chose our triangle by putting different ids in the Set until we found one that we liked. Different triangles provided different test cases and sometimes it was useful to render a few triangles at a time. This filtering was perfectly flexible for us while developing the rest of the algorithm.

So now that we had a single triangle, a lightPath, and a radius, we rendered all those things out to have a look.

At the start of the lightPath, the light is not overlapping the triangle, so the triangle should start out completely dark.

Now that we could see the light, the lightPath and the triangle, we could imagine if the light were to move along the path, it would get closer to the triangle, and eventually cover part, or all of the triangle. Before this point where the triangle should reach “peak brightness,” it would need to begin illuminating.

The light moves along the lightPath and eventually covers the triangle

Some time after reaching peak brightness, the light would move so that it would no longer cover the triangle at all and the triangle would fade back into darkness or extinguish. And finally, the light would reach the end of the lightPath.

At the end of the lightPath, the light is not overlapping the triangle, so our triangle should still be dark.

So we needed to figure out how to tell the triangle how to animate itself based on its relationship to the lightPath that we defined earlier. Before we could determine those relationships, we needed to figure out how to make the triangle change color in the first place.

We knew that we could give our SVG Paths an <animate> tag and give that <animate> tag a list of values to transition between as well as a begin time and a durfor the duration. We also knew that we had 5 keyframes: start, illumination, peak, extinguish, and end. Each keyframe would have a time and a color associated with it. Color at each keyframe would be a function of the distance between the triangle and the lightPath. Time would be a function of how far the light has traveled along the lightPath at these key points.

We thought that we could get away with just those attributes, making our<animate> tags look like this:

However, we didn’t want the light to stop moving. Once the light reached the end of the path, it should turn around and go back to the start. So the triangles, that were once extinguished, would need to illuminate again. That meant we needed to write a whole new <animate> tag for the second animation in the other direction.

Adding a second <animate> tag felt unwieldy, unreadable and difficult to debug. Even though the animation worked, using this approach meant that some of our keyframes were implicit. We decided to look for an alternative.

We found that as an alternative to the begin property, SVG understands a list of keyTimes. This, along with the dur and values properties, allowed us to specify all the keyframes in both time and value, thereby spacing out the animation with a single tag so that the triangle doesn’t light up until the light reaches the illuminationPoint and extinguishes by the extinguishPoint.

Using keyTimes instead of begin was both more concise and more complete. In the case of the triangles near the start and end, using keyTimes results in significantly fewer characters. We are actually saving 17% and 22% of the characters on those lines respectively. In a file that is going to get downloaded to a client’s browser, file size is a big deal so anything we can save is a good thing. Additionally, we were getting those savings while adding information that was implicit before. Now that we knew that this was the format that our <animate> tags needed to end up in, we could determine the states of our keyframes and then translate those states into a keyTimes and a values list.

Calculating Keyframes

We began with the start and end keyframes because they are easy. We already have the start and end points of the lightPath and we know the times corresponding to those points. If our duration is 20 seconds, and we’re just doing a start and end, our keyTimes should just be keyTimes="0;1;0". The values for those two keyframes will be based off the distance that the light is from the triangle.


Calculating the distance from a point to a triangle can be difficult if you consider the triangle’s exact shape. You could take minimum distance to each of the triangle’s three points. However, what if two of those points are equidistant and the third is farther away? Then some point along the side of the triangle is closer than either of those two equidistant points. Instead of dealing with all that complexity, we decided to simply use the centroid of the triangle by averaging the three points in our compute.centroid function.

The centroid of a triangle (yellow)

Now that we had the centroid, we could calculate the distance between the start and the centroid and determine what color the triangle should be when the light is starting. Remember that the light has a radius and that triangles that are outside the radius should simply be black, completely unlit. Triangles within the circle, should be lit up proportional to their closeness to the center. Triangles who’s centroid is exactly on the line should be at the maximum brightness. We accomplished this with our compute.color function.

Now that we had a color function to transform distance into a color value, we could find our other 3 key points and figure out the color values for them too. The remaining 3 points were the illumination point, the peak brightness point, and the extinguish point. Of those three, we started with the peak point.

To find the peak point, we wanted to draw a line from the centroid that intersected the lightPath at a right angle. That gave us the closest point the light would come to our triangle and therefore the time at which the triangle should be the brightest. To find this peak point, we used our pointOnLine function.

Having found our peak point, the only things left to find were the illumination and the extinguish points. Let’s start with the illumination point for no other reason than it is first.

At the illuminationPoint, the light is just starting to overlap the centroid, so the triangle should start fading in.

We knew that the triangle should start illuminating when the center of the light reaches a point along the lightPath such that the centroid of the triangle is sitting on the edge of the light circle. That meant that the distance from the centroid to the center of the light was exactly equal to the radiusof the light.

Knowing that the distance between the centroid and the peak point is one side (B) of a triangle, the radius is the hypotenuse (C) of that triangle, we knew we could get the third side (A) of the triangle by using the pythagorean theorem. That’s exactly what we did in our pythagoreanA function.

PythagoreanA gave us the distance from the peak point to the illuminationpoint, but we didn’t yet have the illumination point itself. To find that, we used the distance and the slope of the lightPath to compute the rise and runfrom the peak point. Then we subtracted the rise and run from the x and y values of the peak point.

While computing the illumination point, we realized that the extinguish distance would be the same as the illumination distance and so we could simply add the same rise and run values to the peak point to get the extinguish point. So we were able to handle both in our surroundingPoints function.

At the extinguishPoint, the light is no longer overlapping the centroid, so the triangle should have finished fading out.

And just like that, we’d found all our key points! We fed them through the color function we wrote earlier to get all the values for our keyframes. But we didn’t yet have a way to get all the times for the keyframes.

To get the times for each keyFrame, we computed the distance from each key point to the start point. Then we compared those distances to the total distance from start to end. Then we used the start and end times to interpolate the time at which the light would reach the key point.

For instance, if our start point is 1,000 pixels away from the end point and duration is 10s, then we know that the light should be reaching the end when t=10s. If the illumination point is 200 pixels away from the start point, then we can calculate that the light will reach the illumination point at t=2sby using the ratio 1000px/10s = 200px/t. We do this ratio calculation in our lerp function.

So now we had a time and color value for all our keyframes, but we only had them going in one direction, from start to end. We needed to mirror those lists so that the animation had a smooth transition from end back to start. However, we weren’t able to mirror both lists the same way.

The color list simply needed to be the same forwards and backwards without repeating the end color. So ["#000", "#fff", "#111"] became ["#000", "#fff", "#111", "#fff", "#000"], where the middle"#111" isn’t repeated. We do this in our colorMirror function.

The time list on the other hand, needed to turn from [0, 0.2, 1] into [0, 0.1, 0.5, 0.9, 1] since keyTimes needs to start at 0 and end at 1. Since we’re sending the animation back to the start, we can halve all the original key times and then mirror the intervals (again, not repeating the middle value). This is handled in our timeMirror function.

Finally, we had all the data for the complete animation we wanted, and we could send it into an updated draw.triangle function.


As mentioned before, we did a couple things to reduce the resulting SVG file size. These optimizations were important because a larger file size results in a longer download time.

The first optimization we made was in the draw.triangle function. We didn’t need to write out <animate> tags for triangles that remained the same color the entire time. Those triangles can simply needed a fill color. We did this with a check on the color list.

By default, JS floating point numbers are 18 characters long. We certainly didn’t need all that precision for our keyTimes, so we rounded those to 2 digits and saved ourselves a bunch.

We were also able to save on our color strings. Initially we were using an hslstring to compute the grayness for us. But we wondered if we could get away with the shortest color string possible, just "#000", "#111", "#aaa" and so on. SVG and browsers understand a 3-digit RGB expression. If we were to use all 3-digit colors, we’d only have 16 grey values. But perhaps that would be fine since the animation would smooth out the in-between colors.

After trying that out, it didn’t look as smooth as we wanted. There were distinct rings of different grays around the light. We needed to be able to specify an in-between grey like "#f4f4f4" in some of our key frames. But in the case where the keyframe color was "#444444", we could still shorten that to the equivalent 3-digit expression of "#444". So we did that in our padColor function called from our compute.color function.

We checked our optimization work by sending it through SVGOMG and it did not detect any further improvements. In fact, our optimizations surpassed those of SVGOMG, as running it though the tool increased the file size.

Unfortunately, a few issues caused us to make our file size larger. We needed to include a second <animate> tag for the "stroke" in addition to the "fill"of the triangle because the edges of the triangles showed up pixelated without the stroke. We also needed to include a "fill" attribute on all the triangles even if they were animated because, even in 2017, Internet Explorer doesn’t support the <animate> tag in SVGs.

Triangles Everywhere

At this point, the only thing left to do was to put the finished SVG in our website project. We included the file as a background-image so that we could set the background-size to "cover" and have it fill whatever space it was in. We then used mix-blend-mode to add some non-grey color to the triangles in different parts of our site.

Homepage theme switcher

Our homepage uses this animation in 3 different sections and our interior pages use it for the headings as well.

Interior page heading

Thanks for taking the time to check out the work we did! Thanks also to my teammates, Kyle GachBrent ErtzRiley Davis and Mike Waite, a few craftsmen who are very good at building nice things.

To check out the finished product, head over to http://mineral-ui.com.

Leave a Reply

Your email address will not be published. Required fields are marked *