Blog

Path Morphing Animation

2024-09-2710 min reading

Kinga Jaworska

Software Engineer

Overview

See how the mathematical prediction method used by ancient civilizations can help you create smooth svg animation

Author: Kinga Jaworska

Date added: 2024-09-27

10 min reading

#Animation

It just sounds scary

Any CSS animation is based on modification of the following characteristics: dimensions, color, objects position, opacity, etc. Nothing special you may admit, until a designer you are working with, decides to make something not trivial and even more, something outstanding. This is where challenges appear.

Imagine you need to transform one shape to another. This process is called a path morphing animation. To make transition between two SVGs more smooth you would have to apply some calculations. If you haven’t got a chance to work with that - feeling the complexity of this task it’s just a first impression you might get.

No worries, in this article I’ll show you how you can do it in a simple way by utilizing appropriate tools.

Flubber in coming

At the services section of Kruko website or at the attached video below you can see the example of path morphing animation.

As you see in the example one shape is changed by another. The reasonable question will be: what should I do to get such an effect? You might want to use Flubber.js. It is a lib that can help you to make proper calculations and animate provided SVG shapes. Don’t be confused with search results by the ‘flubber’ keyword. One of them may look like:

If you have had an opportunity to get to know Framer Motion, you may find in the documentation that it is recommended to use Flubber.js library for path morphing animation.

Flubber is easy to use, well documented and provides best practices for smoothing of 2D shapes. You may ask: “Hey, ok, that's one more animation library but how does it make an animation smooth and more natural and what it is based on?”, and it would be a great question. The short answer will be - smoothing it’s based on interpolation. I believe such an answer will not satisfy you so let's dive a little bit deeper to get a better understanding of interpolation.

Mathematical predictions under the hood

Interpolation is a mathematical method used in different fields of knowledge. It has already been used by ancient civilizations when they were trying to predict the motion of planets based on previous observations. Now it’s used in science eg. in machine learning, computer graphics or analysis to predict a state based on known data.

In our case SVGs paths are a set of commands with values to render certain shapes, so we can also predict unknown data using interpolation. We know the starting shape and destination shape we want to achieve, so the interpolation method helps us to calculate what we should see in the middle of start and destination shapes.

For geeks this is linear interpolation equation:

Y1, Y2, X2, X1 - are values which we know X and Y - are unknown values

Imagine you’re iPhones seller. X2 and X1 are data representing your overall amount of sales from eg. two different months. Y1 and Y2 are your profits from these transactions. And then you want to know

  • what your sales (Y) should be if you want to achieve 1000$ profits (so that will be your X value).

To understand interpolation in a morphing context you can watch Open Vision Conference 2017. It also includes morphing images of orangutans so I really recommend you to watch this video or just skip to the video below to see this reference directly.

src: Image Morphing

Using flubber in a practice

Now let’s get back to animation. First step is to have properly prepared data. You need to provide a path of your SVG (“d” attribute only). Eg. stroke or circle should be replaced by path to get representation of this as a plain string. That could be one of the most time consuming tasks during this challenge 😀.

const path = 'M182.579 146H37.3651C16.7289 146 0 129.2629 0 108.6165c0-20.6463 16.7289-37.3834 37.3651-37.3834 4.556 0 8.9216.8158 12.9588 2.3094C52.5749 50.78 71.7676 33 95.1111 33c18.0359 0 33.5929 10.6134 40.7739 25.9381 4.353-2.9646 9.611-4.6975 15.274-4.6975 15.008 0 27.174 12.1725 27.174 27.188 0 .6705-.024 1.3353-.072 1.9936 1.412-.1941 2.853-.2944 4.318-.2944 17.353 0 31.421 14.0744 31.421 31.4361S199.932 146 182.579 146Z';

It’s also possible to not use the Point[][] type that allows you to provide data arrays: [number, number]. Below is the representation of circle in this way:

[[1,1], [2,1], [1.5,2]]

When your data are well prepared you can take care about installation of flubber by simply using eg. npm

npm install flubber

I'm pretty sure that you’re using TypeScript, so to add proper definition types you should run also command:

npm i ericbf/flubber

Now you’re ready to pass params to flubber function 🥳. Here it’s this one which we use in our website.

export function interpolate(fromShape: Shape, toShape: Shape, options: InterpolateToPointsOptions): (t:number) => string;

So quick legend for that:

fromShape - path of current displayed svg Shape is - string || Point[] type toShape - path of desired SVG options:

  • string?: boolean - you can decide if you want to represent results as an SVG path or array of points
  • maxSegmentLength?: number | false - by default is set to 10. Lower value makes animation smoother but at the expense of performance.

What is the result of this function? As you can see above it returns another function: (t: number) => string. The returned function takes a value t - from 0 to 1 and returns the in-between shape that we want to ‘guess’.

const interpolator = interpolate(renderedPathRef,current, targetPath, {
     maxSegmentLength: ANIMATION_MAX_SEGMENT_LENGTH, 
});
(src: Kruko.io author of code - Bartosz Słysz)

We’re dealing here with a leverage closure pattern. Interpolate returns function that has access to variables from an outer scope. How does it help? If Interpolate returns function, in interpolator we have exactly this structure (t: number) => string. We’re passing fromShape, toShape and options and each time when we call an interpolator it has access to these variables so it's more efficient. Especially for some expensive calculations to create animation.

Then finally you can build components with one props - that will be targetPath.

<svg
    width="500"
    eight="500"
    viewBox="0 0 500 500"
    fill="none"
>
    <path fill="black" d={renderedPath} />
</svg>

Inside this component we have to useEffect() function. Meaningful part is to invoke interpolation with progress, so you’re recursively invoking a function - sheduleRecursiveUpdate which likely calculates the current state of the animation based on this progress. The t value from the previous paragraph represents the progress (value between 0 to 1; 0 - animation start; 1- animation end;).

const [rendreredPath, setRenderedPath] = useState(targetPath);
const renderedPathRef = useRef(renderedPath); 
// reference to display shape we can keep currentPath between rerenders and avoid infinite loop 

const animationFrameId = useRef(-1)

useEffect = (() => {

if(renderedPathRef.current === targetPath){
    return; 
// If the rendered path is already equal to the target path, 
we return early
}



const timestampStart = performance.now();

const sheduleRecursiveUpdate - (currentTimestamp: number) => {
const timeDelta = currentTimestamp - timestampStart;
const progress = Math.max(0, Math.min(1, timeDelta / ANIMATION_DURATION)); // calculate progress of animation

const currentPath = interpolator(progress); 

setRenderedPath(currentPath); // set path for current progress value

renderedPathRef.current = currentPath;

    if(progress < 1) // if we should still change animation
    {
        animatonFrameId.current = 
        requestAnimationFrame(sheduleRecursiveUpdate) // invoke new
        calculation to get new in-between shape for next progress value
    }
};

animatonFrameId.current = requestAnimationFrame(sheduleRecursiveUpdate)

return () => {
    cancelAnimationFrame(animationFrameId.current);
};

},[targetPath])

About requestAnimationFrame you can read in the first article of creative short stories -> Kruko Animation 👀.

What more with flubber library?

Flubber offers other build in methods as:

  • toCircle(), toRect() methods allows you to morphing shape to regular shapes as circle / rectangular
  • Array of points can easily be changed to string path using flubber utils as toPathString()
  • combine() if you want change array of shapes to one shape
  • separate() as combine but changing one shape to array of shapes

Key Takeaways

  • The article provides code examples for using Flubber to achieve path morphing animation.
  • Path morphing describes the process of creating animation by changing one SVG into another.
  • Flubber is a JavaScript library that helps with calculations and creating such animations.
  • Core calculation provided by Flubber relies on interpolation, a method to predict unknown data points between two known ones.
  • Flubber also offers extra functionalities like morphing basic shapes or converting path data.

So count down and try this out on your own 🎉

Let’s build something together