Ripple Shader
A website animation tutorial featuring a ripple shader effect using React Three Fiber, Next.js and React. Inspired by https://homunculus.jp/ and Yuri Artiukh.
Olivier Larose
September 4, 2023
/
Intermediate
/
Medium
A web animation tutorial on how to make a 3D earth rotate on scroll, with a clip path animation using Three.js, Framer Motion and Next.js. Inspired by https://minhpham.design/
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 @studio-freight/lenis
.npm i framer-motion
.npm i framer-motion-3d
.npm i @react-three/fiber
.page.js
page.mod...
globals....
1
import styles from './page.module.scss'2
import React from 'react';3
import dynamic from 'next/dynamic';4
5
const Earth = dynamic(() => import('@/components/earth'), {6
ssr: false,7
loading: () => <img src="/assets/placeholder.png"></img>8
})9
10
export default function Home() {11
return (12
<main className={styles.main}>13
<Earth />14
</main>15
)16
}
Note: We can use the dynamic
import to render an image while the Earth
component is loading.
The earth will be a sphere mesh rendered by r3f.
I will add 3 different textures to make it look good:
I also create a sphere with args{[1, 64, 64]}
, which is the amount of segmentHeight
and segmentWidth
to make the sphere smooth.
components/earth/index.jsx
1
'use client';2
import { Canvas, useLoader } from '@react-three/fiber'3
import { TextureLoader } from 'three/src/loaders/TextureLoader'4
5
export default function earth() {6
7
const [color, normal, aoMap] = useLoader(TextureLoader, [8
'/assets/color.jpg',9
'/assets/normal.png',10
'/assets/occlusion.jpg'11
])12
13
return (14
<Canvas>15
<ambientLight intensity={0.1} />16
<directionalLight intensity={3.5} position={[1, 0, -.25]} />17
<mesh scale={2.5}>18
<sphereGeometry args={[1, 64, 64]}/>19
<meshStandardMaterial map={color} normalMap={normal} aoMap={aoMap}/>20
</mesh>21
</Canvas>22
)23
}
The animation I'd like to have is a rotation on scroll.
It's possible to create that in multiple ways, but today I'll make it using the useScroll
hook from Framer Motion.
components/earth/index.jsx
1
...2
const scene = useRef(null);3
const { scrollYProgress } = useScroll({4
target: scene,5
offset: ['start end', 'end start']6
})7
return (8
<Canvas ref={scene}>9
<motion.mesh scale={2.5} rotation-y={scrollYProgress}>10
...
offset
allows us to track the scroll from the moment we see the Canvas
to the moment don't see it anymorescrollYProgress
results in a value between 0
and 1
However now the animation is not smooth because it is directly linked with the scrollbar.
There are two ways of making the rotation smooth.
useSpring hook
1
const smoothRotation = useSpring(scrollYProgress, {2
damping: 203
});
Or we can also smooth out the whole scrolling, which is what I'll do for this animation.
componen...
page.js
1
'use client';2
import { useEffect } from 'react'3
import Lenis from '@studio-freight/lenis'4
5
export default function smoothScroll({children}) {6
7
useEffect( () => {8
window.scrollTo(0,0);9
10
const lenis = new Lenis()11
12
function raf(time) {13
lenis.raf(time)14
requestAnimationFrame(raf)15
}16
17
requestAnimationFrame(raf)18
}, [])19
20
return children21
}
Couple notes of the code above:
The projects component is made of 2 different animations:
useScroll
, useTransform
and useMotionTemplate
componen...
projects...
1
2
'use client';3
import { useState } from 'react';4
import styles from './style.module.scss';5
import Titles from './titles';6
import Descriptions from './descriptions';7
8
const data = [9
{10
title: "Ford",11
description: "Working on the Next-Generation HMI Experience without no driving experience.",12
speed: 0.513
},14
{15
title: "UFC",16
description: "Developed the Future of UFC Sports Ecosystem despite not being a sports fan.",17
speed: 0.518
},19
{20
title: "Lincoln",21
description: "Defined the visual concept and design language for the Lincoln Zephyr 2022 but never seen it in real life.",22
speed: 0.6723
},24
{25
title: "Royal Caribbean",26
description: "I was just one person on a massive team that created an entire Royal Caribbean eco-system.",27
speed: 0.828
},29
{30
title: "Sleepiq",31
description: "Designed a 1M+ users product utilizing my best personal experience: sleeping.",32
speed: 0.833
},34
{35
title: "NFL",36
description: "Explored the Future of Fantasy Football while being in a country where football means a total different sport.",37
speed: 0.838
}39
]40
41
export default function Projects() {42
const [selectedProject, setSelectedProject] = useState(null)43
return (44
<div className={styles.container}>45
<Titles data={data} setSelectedProject={setSelectedProject}/>46
<Descriptions data={data} selectedProject={selectedProject}/>47
</div>48
)49
}
A state is created to track which project is hovered, allowing the communication between the two components.
The on scroll clip animation requires 3 hooks from the Framer Motion library:
componen...
titles/s...
1
2
import React, { useRef } from 'react'3
import styles from './style.module.scss';4
import { useScroll, motion, useTransform, useMotionTemplate } from 'framer-motion';5
6
export default function index({data, setSelectedProject}) {7
return (8
<div className={styles.titles}>9
{10
data.map( (project, i) => {11
return <Title key={i} data={{...project, i}} setSelectedProject={setSelectedProject}/>12
})13
}14
</div>15
)16
}17
18
function Title({data, setSelectedProject}) {19
20
const { title, speed, i } = data;21
const container = useRef(null);22
23
const { scrollYProgress } = useScroll({24
target: container,25
offset: ['start end', `${25 / speed}vw end`]26
})27
28
const clipProgress = useTransform(scrollYProgress, [0,1], [100, 0]);29
const clip = useMotionTemplate`inset(0 ${clipProgress}% 0 0)`;30
31
return (32
<div ref={container} className={styles.title}>33
<div34
className={styles.wrapper}35
onMouseOver={() => {setSelectedProject(i)}}36
onMouseLeave={() => {setSelectedProject(null)}}37
>38
<motion.p style={{clipPath: clip}}>39
{title}40
</motion.p>41
<p>42
{title}43
</p>44
</div>45
</div>46
)47
}
The hover animation is much simpler, as it only uses CSS and the value of the state to make the animation.
componen...
descript...
1
import React from 'react'2
import styles from './style.module.scss';3
4
export default function index({data, selectedProject}) {5
6
const crop = (string, maxLength) => {7
return string.substring(0, maxLength);8
}9
10
return (11
<div className={styles.descriptions}>12
{13
data.map( (project, i) => {14
const { title, description } = project;15
return (16
<div17
key={i}18
className={styles.description}19
style={{clipPath: selectedProject == i ? "inset(0 0 0)" : "inset(50% 0 50%"}}20
>21
<p>{crop(title, 9)}</p>22
<p>{description}</p>23
</div>24
)25
})26
}27
</div>28
)29
}
We're offically done with this animation!
Very nice to learn from the masters, lots of good concept in this animation, hope you learned something!
-Oli