Телефон Threejs вращается при прокрутке с помощью реакции-три/волокна

Я создал эту модель Threejs в React-Three/Fiber.

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>
    </>
  );
}

Я хочу, чтобы он вращался при прокрутке пользователя, как в этой демонстрации липкого ящика. 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>
  )
}

Могу ли я просто привязать поле ref = к реквизитам компонента?

похоже на это https://codepen.io/kdbkapsere/pen/wvWJmGX


последние коды и коробка 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

-- используя вставку YouTube в сетку

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

🤔 А знаете ли вы, что...
React Native - это фреймворк, основанный на React, для создания мобильных приложений для iOS и Android.


88
1

Ответ:

Решено

Вы можете сделать это несколькими способами. Вы можете использовать какой-нибудь фрейм, чтобы разместить холст как часть прокрутки DOM с другими элементами. Вы можете полностью заполнить представление холстом и создать всю логику прокрутки с нуля, рассчитывая каждое конкретное событие. И это кажется лучшим решением, особенно если убрать родную полосу прокрутки. Однако это также будет иметь свои ограничения, такие как невозможность использования ScrollTrigger, вам придется использовать другой способ прослушивания элементов для входа в интересующую вас часть области просмотра и запуска определенных событий. Это будет иметь большие преимущества, особенно на мобильных устройствах, из-за громоздкого и некрасивого пересчета холста. Но если проект сложный, это отнимает много времени...

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

ПЕСОЧНИЦА

РЕДАКТИРОВАТЬ

Вращение 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>

РЕДАКТИРОВАТЬ 2

Видео текстура

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>
  )
}

Текстура

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>
  )
}