Logo

lycanthropy.dev

Ruby on Rails Engineer

Arbitrary Shape Morphing is Apparently Easy?

While designing this site, I had a vision. I wanted the logos in the header to morph into different shapes when hovered. So I started trying to figure it out, with a few requirements.

  • It must work with two arbitrary shapes
  • It has to use SVGs, so I can use Font Awesome
  • It shouldn’t require me to edit the SVGs at all
  • It shouldn’t require any external software usage

The Quest

I kept finding cool solutions for SVG animation, like Lottie or this random thing on Medium . All of them could do what I wanted. But none of them did it in a way I found particularly appetizing.

There were also some examples using D3 . But to put it bluntly, the D3 examples all looked like garbage.

Then I finally found my saving grace, Noah Veltman and his amazing flubber library. Simply put, Noah is a data visualization wizard. I hightly urge you to go click around his website a bit and see all his awesome projects. My favorites are San Francisco Streets, How to Calculate Pi, and NYPD Complaints.

Poking around flubber’s README revealed that it hits all my target requirements.

  • Arbitrary shapes.. check.
  • Renders to SVG.. check.
  • Doesn’t require editing.. check.
  • No external software.. check.

Plus it looks amazing while doing it:

Flubber in action

If you want to know how flubber works on a deeper level than I’ll explain here, check out Noah’s talk at Open Vis Conf 2017.

Mighty Morphin’ SVG Rangers

All my examples will use D3 to simplify the code. But flubber does work with just plain old vanilla JS.

Check out how easy it is to use:

import { interpolate } from "flubber";
import * as d3 from "d3";

const svgPath1 = getSvgPath1();
const svgPath2 = getSvgPath2();

const interpolator = interpolate(svgPath1, svgPath2);

d3.select("path")
  .transition()
  .attrTween("d", function() { return interpolator });

Dead simple. Now let’s test it out:

And a more advanced case:

Well that works, technically. But it merges the first Octocat into the eye of the second Octocat. Not quite what I expected. What’s going on here?

One to One Mappings

After watching Noah’s talk, I realized the issue. The first Octocat has 1 distinct shape, but the second has three, but there is only one SVG path.

So, flubber does it’s best. It picks one of the shapes in Octocat 2 and tries to morph Octocat 1 into that. It then shows the full SVG after the last frame.

The issue is that there is not a one to one correspondence between shapes in Octocat 1, and shapes in Octocat 2.

Well.. that sounds like a hard problem to solve. There’s probably no super simple easy to use solution right?

Turns out, Noah already solved this problem for us. Flubber comes with some awesome utilities. The ones we care about are splitPathString, combine and separate.

Here’s what they do:

  • separate -> Works like interpolate but for splitting one shape into many.
  • combine -> Works like interpolate but for combining many shapes into one.
  • splitPathString -> Takes an SVG path and does it’s best to split it into many SVG paths.

Using those three utilities we can take any single-shape SVG and morph it into any multi-shape SVG.

A working example of that might look like:

import { combine, separate, splitPathString } from "flubber";
import * as d3 from "d3";

const singleShapePath = getSvgPath1();
const multiShapePaths = splitPathString(getSvgPath2());

// single: true makes these output 1 SVG path, making it easier to use
const forward = separate(singleShapePath, multiShapePaths, { single: true });
const backward = combine(multiShapePaths, singleShapePath, { single: true });

morph(forward);
morph(backward);

function morph(interpolator) {
  d3.select("path")
    .transition()
    .attrTween('d', function() { return interpolator });
}

It’s key to note this still works if there is only one path in the 2nd SVG. Now, let’s see it in action!

It looks amazing! So crisp and fluid. You can see flubber dividing the original X into 3 shapes before morphing into the bars.

Let’s try that GitHub logo example again:

Looks a lot better. But it still doesn’t look quite right.

Holes Holes Holes

You’ll notice again the 2nd Octocat has a quirk. It’s eyes kinda phase into existence at the end. This is because it’s face is one big hole! Flubber can’t handle morphing into a shape that has any bounded regions. If there are bounded regions, it just covers them, and then phases them in at the end.

Depending on what SVGs you choose, this can either look really stupid, or be unnoticable. For my use cases this is good enough. It’s easy enough to use that you can just try a couple different SVG combinations until you get something that looks right.

Let’s put it all together for one final demo,

Perfect. A beautiful, smooth morph between two pretty complex SVGs.

Conclusion

With a few considerations, it’s suprisingly simple to morph between any two SVGs.

While it’s not something that you’ll want to use for every single interaction, it’s an amazing tool to have in your belt.

Big shoutouts again to Noah Veltman. Without his work, none of this would be possible. I again urge you to go check out his stuff and show him some love.

Until the next one,

- Ethan

© 2024 Ethan Kircher
pfp by @frannyanart
design by @dylhack