I have stacked the sticky areas – but how to add the text in the same style/manner. Also I’ve created refs for each of the phone scenes – but its no longer sticking – and I am unsure what causes it to stick in the first place to control how long the user sees each part?
This renders multiple phones – but the sticky aspect has got lost
https://codesandbox.io/p/sandbox/r3f-scroll-rig-sticky-box-forked-3zpm73
import React, { useRef, useEffect } from 'react'
import { Canvas, useThree } from '@react-three/fiber'
import { useGLTF, OrbitControls } from '@react-three/drei'
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
import * as THREE from 'three'
gsap.registerPlugin(ScrollTrigger)
const IphoneModel = (ref) => {
const group = useRef()
const { nodes, materials } = useGLTF('/Iphone15.glb')
useEffect(() => {
const video = document.createElement('video')
video.src = 'https://cdn.pixabay.com/video/2024/07/14/221180_tiny.mp4'
video.crossOrigin = 'anonymous'
video.loop = true
video.muted = true
video.play()
const videoTexture = new THREE.VideoTexture(video)
videoTexture.minFilter = THREE.LinearFilter
videoTexture.magFilter = THREE.LinearFilter
videoTexture.encoding = THREE.sRGBEncoding
materials.Screen.map = videoTexture
materials.Screen.needsUpdate = true
const tl = gsap.timeline({
scrollTrigger: {
trigger: ref[0],
scrub: 1,
markers: true,
pin: true,
start: 'top top',
end: 'bottom top'
}
})
tl.to(group.current.rotation, { z: -Math.PI / 8, duration: 2 })
}, [materials.Screen])
return (
<group ref={group} dispose={null} scale={0.2} rotation={[Math.PI / 2, 0, Math.PI / 8]}>
<mesh geometry={nodes.M_Cameras.geometry} material={materials.cam} />
<mesh geometry={nodes.M_Glass.geometry} material={materials['glass.001']} />
<mesh geometry={nodes.M_Metal_Rough.geometry} material={materials.metal_rough} />
<mesh geometry={nodes.M_Metal_Shiny.geometry} material={materials.metal_Shiny} />
<mesh geometry={nodes.M_Plastic.geometry} material={materials.metal_rough} />
<mesh geometry={nodes.M_Portal.geometry} material={materials['M_Base.001']} />
<mesh geometry={nodes.M_Screen.geometry} material={materials.Screen} />
<mesh geometry={nodes.M_Speakers.geometry} material={materials.metal_rough} />
<mesh geometry={nodes.M_USB.geometry} material={materials.metal_rough} />
</group>
)
}
const Background = () => {
const { scene } = useThree()
useEffect(() => {
scene.background = new THREE.Color('#555555')
}, [scene])
return null
}
const TextSection = () => {
const textRefs = useRef([])
useEffect(() => {
gsap.fromTo(
textRefs.current,
{ opacity: 0 },
{
opacity: 1,
stagger: 0.1,
scrollTrigger: {
trigger: '#text-trigger',
start: 'top bottom',
end: 'center center',
scrub: 1,
markers: false
}
}
)
}, [])
const texts = ['Ready 5', 'Ready 4', 'Ready 3', 'Ready 2', 'Ready 1']
return (
<div
id="text-trigger"
style={{
height: '100vh',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
top: '500px'
}}>
{texts.map((text, index) => (
<h1 key={index} ref={(el) => (textRefs.current[index] = el)} style={{ opacity: 0 }}>
{text}
</h1>
))}
</div>
)
}
const ThreeScene = () => {
const threeSceneGroup = useRef()
return (
<div id="three-canvas-container" style={{ width: '100vw', height: '500px' }}>
<div>
<h2>header text</h2>
<p>text text text</p>
</div>
<Canvas camera={{ position: [0, 0, 10], fov: 45 }} gl={{ antialias: true, alpha: false }}>
<ambientLight intensity={0.4} />
<directionalLight position={[5, 10, 7.5]} intensity={1} />
<IphoneModel ref={threeSceneGroup} />
<OrbitControls enableZoom={false} />
<Background />
</Canvas>
</div>
)
}
const App = () => (
<div style={{ display: 'flex', flexDirection: 'column', height: '400vh' }}>
<div className="some-content" style={{ height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<h1>ACTION</h1>
</div>
<ThreeScene />
<ThreeScene />
<ThreeScene />
<ThreeScene />
<ThreeScene />
<TextSection />
</div>
)
export default App
This renders just one phone but actually has working sticky features.
https://codesandbox.io/p/sandbox/r3f-scroll-rig-sticky-box-forked-jns24q
app.js
import React, { useRef, useEffect } from 'react'
import { Canvas, useThree } from '@react-three/fiber'
import { useGLTF, OrbitControls } from '@react-three/drei'
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
import * as THREE from 'three'
gsap.registerPlugin(ScrollTrigger)
const IphoneModel = () => {
const group = useRef()
const { nodes, materials } = useGLTF('/Iphone15.glb')
useEffect(() => {
const video = document.createElement('video')
video.src = 'https://cdn.pixabay.com/video/2024/07/14/221180_tiny.mp4'
video.crossOrigin = 'anonymous'
video.loop = true
video.muted = true
video.play()
const videoTexture = new THREE.VideoTexture(video)
videoTexture.minFilter = THREE.LinearFilter
videoTexture.magFilter = THREE.LinearFilter
videoTexture.encoding = THREE.sRGBEncoding
materials.Screen.map = videoTexture
materials.Screen.needsUpdate = true
const tl = gsap.timeline({
scrollTrigger: {
trigger: '#three-canvas-container',
scrub: 1,
markers: true,
pin: true,
start: 'top top',
end: 'bottom top'
}
})
tl.to(group.current.rotation, { z: Math.PI * 2, duration: 2 })
}, [materials.Screen])
return (
<group ref={group} dispose={null} scale={0.2} rotation={[Math.PI / 2, 0, -Math.PI / 8]}>
<mesh geometry={nodes.M_Cameras.geometry} material={materials.cam} />
<mesh geometry={nodes.M_Glass.geometry} material={materials['glass.001']} />
<mesh geometry={nodes.M_Metal_Rough.geometry} material={materials.metal_rough} />
<mesh geometry={nodes.M_Metal_Shiny.geometry} material={materials.metal_Shiny} />
<mesh geometry={nodes.M_Plastic.geometry} material={materials.metal_rough} />
<mesh geometry={nodes.M_Portal.geometry} material={materials['M_Base.001']} />
<mesh geometry={nodes.M_Screen.geometry} material={materials.Screen} />
<mesh geometry={nodes.M_Speakers.geometry} material={materials.metal_rough} />
<mesh geometry={nodes.M_USB.geometry} material={materials.metal_rough} />
</group>
)
}
const Background = () => {
const { scene } = useThree()
useEffect(() => {
scene.background = new THREE.Color('#555555')
}, [scene])
return null
}
const TextSection = () => {
const textRefs = useRef([])
useEffect(() => {
gsap.fromTo(
textRefs.current,
{ opacity: 0 },
{
opacity: 1,
stagger: 0.1,
scrollTrigger: {
trigger: '#text-trigger',
start: 'top bottom',
end: 'center center',
scrub: 1,
markers: false
}
}
)
}, [])
const texts = ['Ready 5', 'Ready 4', 'Ready 3', 'Ready 2', 'Ready 1']
return (
<div
id="text-trigger"
style={{
height: '100vh',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
top: '500px'
}}>
{texts.map((text, index) => (
<h1 key={index} ref={(el) => (textRefs.current[index] = el)} style={{ opacity: 0 }}>
{text}
</h1>
))}
</div>
)
}
const ThreeScene = () => (
<div id="three-canvas-container" style={{ width: '100vw', height: '500px' }}>
<Canvas camera={{ position: [0, 0, 10], fov: 45 }} gl={{ antialias: true, alpha: false }}>
<ambientLight intensity={0.4} />
<directionalLight position={[5, 10, 7.5]} intensity={1} />
<IphoneModel />
<OrbitControls enableZoom={false} />
<Background />
</Canvas>
</div>
)
const App = () => (
<div style={{ display: 'flex', flexDirection: 'column', height: '400vh' }}>
<div className="some-content" style={{ height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<h1>ACTION</h1>
</div>
<ThreeScene />
<TextSection />
</div>
)
export default App
I was having issues trying to get the image the right way up — and found this vector2 function did the trick
const imageTexture = new THREE.TextureLoader().load(image)
imageTexture.wrapS = imageTexture.wrapT = THREE.RepeatWrapping
imageTexture.anisotropy = 16
imageTexture.repeat = new THREE.Vector2(1, -1)
https://codesandbox.io/p/sandbox/r3f-scroll-rig-sticky-box-forked-xwt2x6
4
Try something like this, probably in next question you will ask how make animation of showing/hiding of text according to each section… Simply connect opacity: 0=>1=>0
on particular moment of viewport by ScrollTrigger
.
import React, { useRef, useEffect } from 'react';
import { Canvas, useThree } from '@react-three/fiber';
import { useGLTF, OrbitControls } from '@react-three/drei';
import * as THREE from 'three';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
const IphoneModel = ({ modelIndex }) => {
const group = useRef();
const { nodes, materials } = useGLTF('/Iphone15.glb');
useEffect(() => {
const imageTexture = new THREE.TextureLoader().load(
'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/lava/lavatile.jpg'
);
materials.Screen.map = imageTexture;
materials.Screen.needsUpdate = true;
const tl = gsap.timeline({
scrollTrigger: {
trigger: `#section-${modelIndex}`,
scrub: 1,
pin: true,
start: 'top top',
end: 'bottom top',
markers: true
}
});
tl.to(group.current.rotation, { z: Math.PI * 2, duration: 2 });
}, [materials.Screen, modelIndex]);
return (
<group ref={group} dispose={null} scale={0.2} rotation={[Math.PI / 2, 0, 0]}>
<mesh geometry={nodes.M_Cameras.geometry} material={materials.cam} />
<mesh geometry={nodes.M_Glass.geometry} material={materials['glass.001']} />
<mesh geometry={nodes.M_Metal_Rough.geometry} material={materials.metal_rough} />
<mesh geometry={nodes.M_Metal_Shiny.geometry} material={materials.metal_Shiny} />
<mesh geometry={nodes.M_Plastic.geometry} material={materials.metal_rough} />
<mesh geometry={nodes.M_Portal.geometry} material={materials['M_Base.001']} />
<mesh geometry={nodes.M_Screen.geometry} material={materials.Screen} />
<mesh geometry={nodes.M_Speakers.geometry} material={materials.metal_rough} />
<mesh geometry={nodes.M_USB.geometry} material={materials.metal_rough} />
</group>
);
};
const Background = () => {
const { scene } = useThree();
useEffect(() => {
scene.background = new THREE.Color('#555555');
}, [scene]);
return null;
};
const ThreeScene = ({ modelIndex }) => (
<div
id={`three-canvas-container-${modelIndex}`}
style={{ position: 'absolute', top: 0, right: 0, width: '50vw', height: '100vh' }}
>
<Canvas camera={{ position: [0, 0, 10], fov: 45 }} gl={{ antialias: true, alpha: false }}>
<ambientLight intensity={0.4} />
<directionalLight position={[5, 10, 7.5]} intensity={1} />
<IphoneModel modelIndex={modelIndex} />
<OrbitControls enableZoom={false} />
<Background />
</Canvas>
</div>
);
const Section = ({ title, content, modelIndex }) => (
<div id={`section-${modelIndex}`} style={{ display: 'flex', alignItems: 'center', height: '100vh', position: 'relative' }}>
<div style={{ width: '50vw', padding: '0 2rem' }}>
<h1>{title}</h1>
<p dangerouslySetInnerHTML={{ __html: content }} />
</div>
<ThreeScene modelIndex={modelIndex} />
</div>
);
const App = () => (
<div style={{ height: '300vh' }}>
<Section
modelIndex={1}
title="First Section"
content="Lorem Ipsum is simply dummy text of<br />the printing and typesetting industry."
/>
<Section
modelIndex={2}
title="Second Section"
content="Lorem Ipsum is simply dummy text of<br />the printing and typesetting industry."
/>
<Section
modelIndex={3}
title="Third Section"
content="Lorem Ipsum is simply dummy text of<br />the printing and typesetting industry."
/>
</div>
);
export default App;
10
This is the closest I have got so far — but I am unsure what is controlling the sticky and also why the last image is only being loaded in all of the phones.
https://codesandbox.io/p/sandbox/r3f-scroll-rig-sticky-box-forked-hsq9xh
import React, { useRef, useEffect } from 'react'
import { Canvas, useThree } from '@react-three/fiber'
import { useGLTF, OrbitControls } from '@react-three/drei'
import * as THREE from 'three'
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
gsap.registerPlugin(ScrollTrigger)
const IphoneModel = ({ image, modelIndex }) => {
const group = useRef()
const { nodes, materials } = useGLTF('/Iphone15.glb')
useEffect(() => {
//const imageTexture = new THREE.TextureLoader().load(
// 'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/lava/lavatile.jpg'
//)
const imageTexture = new THREE.TextureLoader().load(image)
imageTexture.wrapS = imageTexture.wrapT = THREE.RepeatWrapping
imageTexture.anisotropy = 16
imageTexture.repeat = new THREE.Vector2(1, -1)
//var material = new THREE.MeshPhongMaterial({ map: imageTexture })
materials.Screen.map = imageTexture
materials.Screen.needsUpdate = true
const tl = gsap.timeline({
scrollTrigger: {
trigger: `#section-${modelIndex}`,
scrub: 4,
pin: true,
start: 'top top',
end: 'bottom top',
markers: true
}
})
tl.to(group.current.rotation, { z: Math.PI / 8, duration: 6 })
}, [materials.Screen, modelIndex])
return (
<group ref={group} dispose={null} scale={0.2} rotation={[Math.PI / 2, 0, -Math.PI / 8]}>
<mesh geometry={nodes.M_Cameras.geometry} material={materials.cam} />
<mesh geometry={nodes.M_Glass.geometry} material={materials['glass.001']} />
<mesh geometry={nodes.M_Metal_Rough.geometry} material={materials.metal_rough} />
<mesh geometry={nodes.M_Metal_Shiny.geometry} material={materials.metal_Shiny} />
<mesh geometry={nodes.M_Plastic.geometry} material={materials.metal_rough} />
<mesh geometry={nodes.M_Portal.geometry} material={materials['M_Base.001']} />
<mesh geometry={nodes.M_Screen.geometry} material={materials.Screen} />
<mesh geometry={nodes.M_Speakers.geometry} material={materials.metal_rough} />
<mesh geometry={nodes.M_USB.geometry} material={materials.metal_rough} />
</group>
)
}
const Background = () => {
const { scene } = useThree()
useEffect(() => {
scene.background = new THREE.Color('#555555')
}, [scene])
return null
}
const ThreeScene = ({ image, modelIndex }) => (
<div id={`three-canvas-container-${modelIndex}`} style={{ position: 'absolute', top: 0, right: 0, width: '50vw', height: '100vh' }}>
<Canvas camera={{ position: [0, 0, 10], fov: 45 }} gl={{ antialias: true, alpha: false }}>
<ambientLight intensity={0.4} />
<directionalLight position={[5, 10, 7.5]} intensity={1} />
<IphoneModel image={image} modelIndex={modelIndex} />
<OrbitControls enableZoom={false} />
<Background />
</Canvas>
</div>
)
const Section = ({ image, title, content, modelIndex }) => (
<div id={`section-${modelIndex}`} style={{ display: 'flex', alignItems: 'center', height: '100vh', position: 'relative' }}>
<div style={{ width: '50vw', padding: '0 2rem' }}>
<h1>{title}</h1>
<p dangerouslySetInnerHTML={{ __html: content }} />
</div>
<ThreeScene image={image} modelIndex={modelIndex} />
</div>
)
const json = [
{
id: 1,
title: '1',
content: 'Lorem Ipsum is simply dummy text of<br />the printing and typesetting industry.',
image: 'https://cdn.pixabay.com/photo/2024/09/05/17/31/moutains-9025523_1280.jpg'
},
{
id: 2,
title: '2',
content: 'Lorem Ipsum is simply dummy text of<br />the printing and typesetting industry.',
image: 'https://cdn.pixabay.com/photo/2023/09/29/14/58/road-8284023_1280.jpg'
},
{
id: 3,
title: '3',
content: 'Lorem Ipsum is simply dummy text of<br />the printing and typesetting industry.',
image: 'https://cdn.pixabay.com/photo/2024/08/18/14/34/folkstone-8978132_1280.jpg'
},
{
id: 4,
title: '4',
content: 'Lorem Ipsum is simply dummy text of<br />the printing and typesetting industry.',
image: 'https://cdn.pixabay.com/photo/2024/05/19/18/22/fruit-8773085_1280.jpg'
}
]
const App = () => (
<div style={{ height: '300vh' }}>
{json.map((item, index) => (
<Section key={index} image={item.image} modelIndex={item.id} title={item.title} content={item.content} />
))}
</div>
)
export default App
—
apparently its a screen cloning issue — but the first item is blank
https://codesandbox.io/p/sandbox/r3f-scroll-rig-sticky-box-forked-4qpp6s
let screenMaterial = materials.Screen.clone()
screenMaterial.map = imageTexture
//materials.Screen.map = imageTexture
materials.Screen = screenMaterial
materials.Screen.needsUpdate = true
3