Morph SVG

Popmotion animations can be used to morph between any two SVG path shapes.

SVG-morphing libraries Flubber and Polymorph both functions that accept a value between 0 and 1 to blend between two shapes. This works great with Popmotion’s functional API, and animating the morph is as simple as this:

tween()
  .pipe(morph)
  .start(styler(pathElement).set('d'));

In this quick tutorial we’ll define this morph function with both Flubber and Polymorph and animate it with a tween.

Set up

First, we need an SVG with a path element to render into.

<svg viewBox="0 0 400 400">
  <path id="target" />
</svg>

As we’re going to animate the path‘s verbosely-named d attribute, which defines the shape of the path, we can use Popmotion’s styler function to create an optimised setter:

const path = document.getElementById('#target');
const setPathAttr = styler(path).set('d');

Now we simply need a path definition string to provide to setPathAttr.

Flubber

Flubber is a fully-featured morphing library that yields very accurate and pleasing results. The drawback being its filesize: A whopping 53kb!

Once you’ve installed and imported Flubber according to the docs, you can make the morph function using flubber.interpolate.

This function accepts two strings, one of each path you want to morph between:

const star = "M103.04 162.52L39.36 196l12.16-70.9L0 74.86 71.2 64.5 103.04 0l31.85 64.52 71.2 10.35-51.57 50.22L166.7 196z";
const circle = "M86,171.5 C38.7796539,171.5 0.5,133.220346 0.5,86 C0.5,38.7796539 38.7796539,0.5 86,0.5 C133.220346,0.5 171.5,38.7796539 171.5,86 C171.5,133.220346 133.220346,171.5 86,171.5 Z";

const morph = flubber.interpolate(circle, star);

By default, the blending of the two shapes is pretty rough. We can pass interpolate a third options argument { maxSegmentLength: 2 } to output more accurate shapes, though this might have performance impacts with more complex blends.

The generated morph function accepts a number between 0 and 1 and outputs a path definition string generated from a blend of circle and star.

So to animate our target path, we simply need to write an animation that outputs a number between 0 and 1 and provide morph to its pipe method so its output is the new path:

tween()
  .pipe(morph)
  .start(setPathAttr);

Polymorph

Polymorph is a much lighter library clocking in at just 6kb! For complex shapes it works just as well as Flubber, though results may vary for simpler shapes.

Setup works a little differently as it takes path definitions from existing elements, so we’ll need to add those:

<svg>
  <path id="Oval" d="M86,171.5 C38.7796539,171.5 0.5,133.220346 0.5,86 C0.5,38.7796539 38.7796539,0.5 86,0.5 C133.220346,0.5 171.5,38.7796539 171.5,86 C171.5,133.220346 133.220346,171.5 86,171.5 Z" />
  <path id="Star" d="M103.04 162.52L39.36 196l12.16-70.9L0 74.86 71.2 64.5 103.04 0l31.85 64.52 71.2 10.35-51.57 50.22L166.7 196z" />
</svg>

We simply provide the element ids to polymorph.interpolate, along with an optional precision setting:

const morph = polymorph.interpolate(['#Star', '#Oval'], { precision: 4 });

tween()
  .pipe(morph)
  .start(setPathAttr);

Next steps

Popmotion, Flubber and Polymorph’s functional approach gives you the freedom to combine whichever libraries suit your project.

In this article, we’ve shown how simple it is to animate between shapes using tween. You could play around with other animations like pointer, which would make the blending scrubbable:

pointer()
  .pipe(
    ({ x }) => x,
    interpolate([0, window.innerWidth], [0, 1])
  )
  .start()

One caveat, however, is neither library accepts numbers outside of 0 and 1, which entails the following:

  • spring needs to be overdamped by choosing a damping property high enough to prevent overshoot.
  • Easing functions that generate overshoot, like backOut, will lead to stunted animations.

You can of course pipe animations through the clamp transformer for safety, but ideally you’ll want to create animations that don’t exceed these limits.