profile picture of Olivier Larose

Olivier Larose

September 4, 2023

/

Intermediate

/

Medium

3D Earth

Build a 3D Earth with Smooth Scroll Rotation using Three.js, Framer Motion and Next.js

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 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.

Setting up the page.js

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 Lenis Scroll for the smooth scroll, so we can run npm i @studio-freight/lenis.
  • We will use Framer Motion for the animations, so we can run npm i framer-motion.
  • We will use Framer Motion 3D for the animations as well, so we can run npm i framer-motion-3d.
  • We will use React Three Fiber for the 3D, so we can run 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.

Creating the Earth

The earth will be a sphere mesh rendered by r3f.

I will add 3 different textures to make it look good:

  • Color map: The most common kind of texture map. It defines the color and pattern of the object.
  • Normal map: A texture mapping technique used for faking the lighting of bumps and dents.
  • Occlusion map: Greyscale image, with white indicating areas that should receive full indirect lighting, and black indicating no indirect lighting.

Color Map

Screenshot of the Color

Normal Map

Screenshot of the Normal

Occlusion Map

Screenshot of the Occlusion

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

}

We should have something like this:

Screenshot of the Earth

Sphere Animation

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

...
  • Here the offset allows us to track the scroll from the moment we see the Canvas to the moment don't see it anymore
  • The scrollYProgress results in a value between 0 and 1

However now the animation is not smooth because it is directly linked with the scrollbar.

Smoothing the rotation

There are two ways of making the rotation smooth.

  • The useSpring hook from Framer Motion to smooth out the scrollYProgress value
  • Smooth out the whole scrolling

useSpring hook

1

const smoothRotation = useSpring(scrollYProgress, {

2

damping: 20

3

});

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 children

21

}

Couple notes of the code above:

  • The smooth scroll is created using the Lenis Scroll Library.
  • The whole page is wrapped inside the SmoothScroll wrapper.

We should have something like this:

Projects Layout

The projects component is made of 2 different animations:

  • On Scroll Clip animation (titles): Made with Framer Motion, using the useScroll, useTransform and useMotionTemplate
  • On Hover Clip animation (descriptions): Made with a state and conditional CSS

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.5

13

},

14

{

15

title: "UFC",

16

description: "Developed the Future of UFC Sports Ecosystem despite not being a sports fan.",

17

speed: 0.5

18

},

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.67

23

},

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.8

28

},

29

{

30

title: "Sleepiq",

31

description: "Designed a 1M+ users product utilizing my best personal experience: sleeping.",

32

speed: 0.8

33

},

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.8

38

}

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.

Setting up the titles (on scroll clip)

The on scroll clip animation requires 3 hooks from the Framer Motion library:

  • useScroll: Tracks the progress of the scrol.l
  • useTransform: Transforms a value into another value.
  • useMotionTemplate: Create a string based value from multiple motion values.

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

<div

34

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

}

We should have something like this:

Setting up the descrptions (on hover clip)

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

<div

17

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 should have something like this:

Wrapping up

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

Related Animations

image

April 21, 2024

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.

image

April 21, 2024

Bulge Effect

A website tutorial featuring a bulge distortion animation, made with a shader in GLSL, using React Three Fiber, Next.js and React.

image

April 18, 2024

3D Wave on Scroll

A website animation tutorial featuring a vertex shader with a wave animation applied on a plane. Made with React-three-fiber, Framer Motion and Next.js