I've created this threejs model in react-three/fiber
import React, { Suspense, useRef } from "react";
import { Canvas } from "@react-three/fiber";
import { Environment } from "@react-three/drei";
import { OrbitControls, Stage } from "@react-three/drei";
import Model from "./Model";
export default function App() {
const ref = useRef();
const overlay = useRef();
const caption = useRef();
const scroll = useRef(0);
return (
<>
<Canvas
shadows
eventSource={document.getElementById("root")}
eventPrefix="client"
>
<ambientLight intensity={1} />
<Suspense fallback={null}>
<Model scroll={scroll} />
<Environment preset="city" />
</Suspense>
<OrbitControls ref={ref} autoRotate />
</Canvas>
</>
);
}
I want to get it to have it rotate on user scroll like this stickybox demo https://codesandbox.io/p/sandbox/r3f-scroll-rig-sticky-box-w5v4u7
function SpinningBox({ scale, scrollState, inViewport }) {
const box = useRef()
const size = scale.xy.min() * 0.5
useFrame(() => {
box.current.rotation.y = scrollState.progress * Math.PI * 2
})
const spring = useSpring({
scale: inViewport ? size : size * 0.0,
config: inViewport ? config.wobbly : config.stiff,
delay: inViewport ? 100 : 0
})
return (
<AnimatedRoundedBox ref={box} {...spring}>
<meshNormalMaterial />
</AnimatedRoundedBox>
)
}
Would I just bind a ref = box to the props of the component?
similar to this https://codepen.io/kdbkapsere/pen/wvWJmGX
latest codesandbox https://codesandbox.io/p/sandbox/r3f-scroll-rig-sticky-box-forked-4zj88r
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 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 / 8, duration: 2 })
}, [])
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
-- using a youtube embed into the mesh
<mesh geometry={nodes.M_Screen.geometry} material={materials.Screen}>
<Html occlude="true" transform rotation={[-Math.PI / 2, 0, 0]} position={[0, 0.6, 0]} scale={[1, 1, 1]}>
<div style={{ width: '278px', height: '580px', background: 'white', borderRadius: '50px' }}>
<iframe
src="https://www.youtube.com/embed/oGtQKF62ZYg"
style={{ width: '100%', height: '100%', border: 'none', borderRadius: 'inherit' }}
title="video"
/>
</div>
</Html>
</mesh>
using local video
<mesh geometry={nodes.M_Screen.geometry} material={materials.Screen}>
<Html occlude="true" transform rotation={[-Math.PI / 2, 0, 0]} position={[0, 0.6, 0]} scale={[1, 1, 1]}>
<div style={{ width: '278px', height: '580px', background: 'white', borderRadius: '50px' }}>
<video width="278" height="580" autoplay style={{ width: '278px', height: '580px', background: 'white', borderRadius: '50px' }}>
<source
src="https://videos.ctfassets.net/f1onadsih6xk/5xdqjOZgZHvYos0cTRB4D6/4b5a6b47bc9e46d5ed3bfc8edd780da6/DocumentInSeconds.mp4"
type="video/mp4"
/>
Your browser does not support the video tag.
</video>
</div>
</Html>
Better way of adding the video so the speaker part still shows is modifying the screen material.
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])
You can do this in several ways. You can use some frame to place the canvas as part of a scrolling DOM
with other elements. You can completely fill the view with the canvas and create the entire scrolling logic from scratch with each specific event calculated. And this seems to be the best solution, especially when you remove the native scrollbar. However, this will also have its limitations, such as the inability to use ScrollTrigger
, you will have to use another way of listening for elements to enter the part of the viewport you are interested in, and firing specific events. This will have great benefits, especially on mobile devices, with the cumbersome and unsightly recalculation of the canvas. But with a complex project, it's time-consuming...
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 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 })
}, [])
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 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
EDIT
Rotating iFrame
<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}>
<Html occlude="true" transform rotation={[-Math.PI / 2, 0, 0]} position={[0, 0.6, 0]} scale={[1, 1, 1]}>
<div style={{ width: '278px', height: '580px', background: 'white', borderRadius: '50px' }}>
<iframe
src="https://www.youtube.com/embed/oGtQKF62ZYg"
style={{ width: '100%', height: '100%', border: 'none', borderRadius: 'inherit' }}
title="video"
/>
</div>
</Html>
</mesh>
<mesh geometry={nodes.M_Speakers.geometry} material={materials.metal_rough} />
<mesh geometry={nodes.M_USB.geometry} material={materials.metal_rough} />
</group>
EDIT 2
Video texture
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, 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}>
{/* Video Texture */}
</mesh>
<mesh geometry={nodes.M_Speakers.geometry} material={materials.metal_rough} />
<mesh geometry={nodes.M_USB.geometry} material={materials.metal_rough} />
</group>
)
}
Texture
const IphoneModel = () => {
const group = useRef()
const { nodes, materials } = useGLTF('/Iphone15.glb')
useEffect(() => {
const imageTexture = new THREE.TextureLoader().load(
'https://pixabay.com/get/gc7b67400496e98a63a3c56eae484aa0bbb9163a92866e2588bcf817c8f164853cf8b7153fc1710f8b90ab5e27c9efd8b24dda4bd9238ef937a5474638ceba83536155b6ac9bf2b91f8eebcad2d36519c_640.jpg'
)
materials.Screen.map = imageTexture
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, 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}>
{/* Texture */}
</mesh>
<mesh geometry={nodes.M_Speakers.geometry} material={materials.metal_rough} />
<mesh geometry={nodes.M_USB.geometry} material={materials.metal_rough} />
</group>
)
}