profile picture of Olivier Larose

Olivier Larose

August 29, 2023

/

Intermediate

/

Medium

SVG Morph

How to Make SVG Morph using Framer Motion, Flubber.js and Nextjs

A tutorial that takes a look at using Framer Motion mixer with Flubber.js to create an SVG Morph animation, inside of a Next js project.

Live DemoSource codeVideo Tutorial
background video

Initializing the project

Let's start the project by creating a Next.js application. We can do that by running npx create-next-app@latest client inside of a terminal.

Adding the HTML and CSS

We can delete everything in the page.js , global.css and page.module.css and add our own HTML and CSS, to start with a nice blank application.

  • We will use Sass for the stylesheets, so we can run npm i sass.
  • We will use Framer Motion for the animation, so we can run npm i framer-motion.
  • We will use Flubber js for the SVG Morph, so we can run npm i flubber.

Page Component

page.js

page.mod...

1

import styles from './page.module.scss'

2

import Play from '../components/play';

3

import Smile from '../components/smile';

4

5

export default function Home() {

6

return (

7

<main className={styles.main}>

8

<div className={styles.container}>

9

<Smile />

10

<Play />

11

</div>

12

</main>

13

)

14

}

The Limitations when Morphing SVGs

If you delve in the world of SVG morphing, you'll soon realize how messy it is. The tech is just not there yet. There are a lot of limitations and lots of unexpected behaviors.

One thing to keep in mind is that morphing from an SVG to another SVG that both have the same amount of points is very easy to do. You can use any libraries like Framer Motion, Animate.js or GSAP to do that.

However, I personally would like to morph any shape to another shape of my choice and that's where the limitation comes in. Once you start morphing two completely different shapes, you'll start having jumps, bugs and inversions. To fix that problem, some people have created an algorythm that will try to guess an interpolation between two shapes.

Some of those libraries are GSAP (not free) and Flubber.js (I'll take a look at this library in this tutorial).

Preparing the SVGs

Here's how we can structure our SVGs to be able to morph them.

Screenshot of the HTML and CSS results

Couple notes about exporting the SVG

  • The SVG should be separated in singular shapes
  • Each SVG item should be in the form of a <path/>
Screenshot of the HTML and CSS results

Then we can extract the items in a JS file

play/paths.js

1

export const shape1 = "m0,0h53v178H0V0Z";

2

export const shape2 = "m91,0h53v178h-53V0Z";

3

export const shape1_morphed = "m70.45,134.74l-57.68,43.26V0l56.48,42.36,1.19,92.38Z";

4

export const shape2_morphed = "m65.52,39.56l67.58,49.44-67.58,49.44V39.56Z";

Morphing with Flubber.js

Flubber.js Interpolate function

1

var interpolator = flubber.interpolate(triangle, octagon);

2

3

interpolator(0); // returns an SVG triangle path string

4

interpolator(0.5); // returns something halfway between the triangle and the octagon

5

interpolator(1); // returns an SVG octagon path string

I'll start with vanilla Flubber.js so we can understand what's happening before jumping with Framer Motion.

play/ind...

play/sty...

1

import styles from './style.module.scss';

2

import { shape1, shape2, shape1_morphed, shape2_morphed } from './paths';

3

import SVGMorph from '../svgMorph';

4

5

export default function index() {

6

return (

7

<div className={styles.svgContainer}>

8

<svg className={styles.svg} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 144 178">

9

<SVGMorph paths={[shape1, shape1_morphed, shape1]}/>

10

<SVGMorph paths={[shape2, shape2_morphed, shape2]}/>

11

</svg>

12

</div>

13

)

14

}

Here we start using the Flubber.js library. We use the Interpolate function to shift from one shape to another, we do so in a loop.

svgMorph/index.jsx

1

'use client';

2

import { interpolate } from 'flubber';

3

import React, { useState, useEffect, useRef } from 'react'

4

5

export default function SVGMorph({paths}) {

6

7

const [pathIndex, setPathIndex] = useState(0);

8

const path = useRef(null);

9

10

useEffect( () => {

11

setTimeout( () => {

12

const interpolator = interpolate(paths[pathIndex], paths[pathIndex + 1], {maxSegmentLength: 1});

13

const targetPath = interpolator(1);

14

path.current.setAttribute("d", targetPath);

15

16

if(pathIndex === paths.length - 2){

17

setPathIndex(0);

18

}

19

else{

20

setPathIndex(pathIndex + 1)

21

}

22

}, 1000)

23

}, [pathIndex])

24

25

return (

26

<path ref={path} fill="white"/>

27

)

28

}

Couple notes about the above code

  • At every second, we interpolate from the first given shape to the second given shape and we loop the animation.

We should have something like this:

Animating with Framer Motion

We were morphing from one shape to another without any easings, but can we fix that by using Framer Motion.

We'll use a bunch of different methods from the library:

  • motion: we add a motion tag in front of the path to be able to animate it.
  • animate: an imperative method to animate a motionValue.
  • useMotionValue: a Framer Motion Object that contains a value that can be animated using the animate function.
  • useTransform: a method to transform a motionValue into another one.
  • mixer: a method to mix between each set of output value (we will use it with Flubber.js)

svgMorph/index.jsx

1

'use client';

2

import { interpolate } from 'flubber';

3

import React, { useState, useEffect } from 'react'

4

import { motion, animate, useMotionValue, useTransform } from 'framer-motion';

5

6

export default function SVGMorph({paths}) {

7

8

const [pathIndex, setPathIndex] = useState(0);

9

const progress = useMotionValue(pathIndex);

10

11

const arrayOfIndex = paths.map( (_, i) => i )

12

const path = useTransform(progress, arrayOfIndex, paths, {

13

mixer: (a, b) => interpolate(a, b, {maxSegmentLength: 1})

14

})

15

16

useEffect( () => {

17

const animation = animate(progress, pathIndex, {

18

duration: 0.4,

19

ease: "easeInOut",

20

delay: 0.5,

21

onComplete: () => {

22

if(pathIndex === paths.length - 1){

23

progress.set(0);

24

setPathIndex(1);

25

}

26

else{

27

setPathIndex(pathIndex + 1);

28

}

29

}

30

})

31

return () => {animation.stop()}

32

}, [pathIndex])

33

34

return (

35

<motion.path fill="white" d={path}/>

36

)

37

}

Couple notes about the code

  • Line 9: We create a progress value that will range from 0 to 2 (paths.length - 1). I use it as the main value that will drive the animation, we animate the value with a certain ease with the animate function and use that value as the base value for the useTransform function.
  • Line 11-12-13: We use the progress value to transform one path to the next. The mixer option is used in combination with the interpolate function from Flubber.js to morph the svgs. Note, the mixer function will call the interpolate() function with a p value (0 to 1) which is equal to the progress of the mix and that result is returned in the const path.

We should have something like this:

Adding the smile

The smile is very similar to the play icon, but instead it has 3 morphing elements.

smile/in...

smile/pa...

1

import React from 'react'

2

import { head, smile, eye_l, eye_r, happy_smile, happy_eye_l, happy_eye_r } from './paths';

3

import styles from './style.module.scss';

4

import SVGMorph from '../svgMorph';

5

export default function index() {

6

return (

7

<div className={styles.svgContainer}>

8

<svg className={styles.svg} viewBox="0 0 192 192">

9

<path d={head} fill="white"/>

10

<SVGMorph paths={[smile, happy_smile, smile]}/>

11

<SVGMorph paths={[eye_l, happy_eye_l, eye_l]}/>

12

<SVGMorph paths={[eye_r, happy_eye_r, eye_r]}/>

13

</svg>

14

</div>

15

)

16

}

We should have something like this:

Wrapping up

That's it for this animation!

Flubber.js is a very powerful library and in combination with Framer Motion, it is possible to create all kinds of super fun interactivity. Hope you learned something!

-Oli