Olivier Larose
August 29, 2023
/
Intermediate
/
Medium
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 TutorialLet'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.
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.
npm i sass
.npm i framer-motion
.npm i flubber
.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
}
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).
Here's how we can structure our SVGs to be able to morph them.
Couple notes about exporting the SVG
<path/>
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";
Flubber.js Interpolate function
1
var interpolator = flubber.interpolate(triangle, octagon);2
3
interpolator(0); // returns an SVG triangle path string4
interpolator(0.5); // returns something halfway between the triangle and the octagon5
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
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
(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.p
value (0 to 1) which is equal to the progress of the mix and that result is returned in the const path
.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
}
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