0

I've created this threejs model in react-three/fiber

enter image description here

enter image description here

https://codesandbox.io/p/sandbox/r3f-scroll-rig-sticky-box-forked-7nkhpn?file=%2Fsrc%2FModel.js%3A9%2C30

https://7nkhpn.csb.app/

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])
5

2 Answers 2

1

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

SANDBOX

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>
  )
}
Sign up to request clarification or add additional context in comments.

10 Comments

this is great - I think I got it close to the rotations as a reference - 4zj88r.csb.app -- how would you modify this to have an image/video in the phone - so it all turns together - we have to create a new mesh inside the group?
I'm not sure on that - codesandbox.io/p/sandbox/…
I don't know - I just modified some of the rotation -I'll put the code in the body of the post
That's really good - thank you.
Sure you can, to manege them individually, that should not impact performance to much, of course depends how many you need them ;). Look at some tips here
|
0

This is the closest I have got so far - 20th Sep see deep example -- https://www.exo.inc/iris

enter image description here

  • replacing image with video on half cycle (when the phone is turned around)
  • adding the egg shell gradient background
  • adding the svg morph semi-circle basin to diamond

it renders a video and spins around twice in the same direction

https://codesandbox.io/p/sandbox/r3f-scroll-rig-sticky-box-forked-279mp3

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 = (props) => {
  const group = useRef()
  const { nodes, materials } = useGLTF('/Iphone15.glb')

  useEffect(() => {
    const video = document.createElement('video')
    video.src = props.mediaAsset
    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

    videoTexture.wrapS = videoTexture.wrapT = THREE.RepeatWrapping
    videoTexture.anisotropy = 16
    videoTexture.repeat = new THREE.Vector2(1, -1)

    materials.Screen.map = videoTexture

    let screenMaterial = materials.Screen.clone()
    screenMaterial.map = videoTexture

    //materials.Screen.map = imageTexture
    materials.Screen = screenMaterial

    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 * 4, 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} />
      <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 = (mediaAsset) => (
  <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 mediaAsset={mediaAsset.mediaAsset} />
      <OrbitControls enableZoom={false} />
      <Background />
    </Canvas>
  </div>
)

const App = () => {
  let item = { videoUrl: 'https://cdn.pixabay.com/video/2020/06/04/41128-427876270_large.mp4' }

  return (
    <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 mediaAsset={item.videoUrl} />
      <TextSection />
    </div>
  )
}

export default App

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.