Телефон 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>

Первая песочница не работает.

Łukasz D. Mastalerz 22.08.2024 13:18

Я не знаю, почему это так - у меня код работает.

The Old County 22.08.2024 19:56

Я пытаюсь понять вашу проблему, но мне сложно понять эту гифку, а код, который вы вставили, только меняет ваше вращение. Я думаю, попробуйте создать новую песочницу и вставить URL-адрес или уточнить ожидания от тура при редактировании сообщения.

Łukasz D. Mastalerz 26.08.2024 11:38

Ну еще нужно добавить видео/изображение - videos.ctfassets.net/f1onadsih6xk/1SswaYYtFZR6dTOsFMurFW/… или/и videos.ctfassets.net/f1onadsih6xk/1v3hNplvIPRuHnIW7qpIpd/…

The Old County 26.08.2024 14:54

Позже я понял, о чем вы говорили, проверьте меня, пожалуйста.

Łukasz D. Mastalerz 26.08.2024 14:58
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
0
5
88
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 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>
  )
}

это здорово - я думаю, что в качестве ссылки я получил это близко к поворотам - 4zj88r.csb.app - как бы вы изменили это, чтобы в телефоне было изображение/видео - чтобы все сложилось вместе - у нас есть создать новую сетку внутри группы?

The Old County 26.08.2024 08:59

Я не уверен в этом — codeandbox.io/p/sandbox/…

The Old County 26.08.2024 10:15

Я не знаю - я только что изменил ротацию - я помещу код в тело сообщения

The Old County 26.08.2024 11:05

Я видел на справочном сайте, который просматривал - его загрузка в видео из тега img --- <img src = "videos.ctfassets.net/f1onadsih6xk/5xdqjOZgZHvYos0cTRB4D‌​6/…" alt = "Document видео за секунды" loading = "lazy">

The Old County 26.08.2024 15:00

Да, но я бы предложил localhost mp4 - тогда как встраивание на YouTube - потому что размер, скорее всего, соответствует размеру мобильного устройства - если бы это была модель ноутбука, широкоэкранный YouTube мог бы подойти - хорошо бы выставить его как встраивание, отличное от YouTube. - просто используя mp4 или изображение

The Old County 26.08.2024 20:50

Мне удалось использовать тег видео – я заметил в вашей вставке на YouTube и метод тега видео — он фактически закрывает динамики телефона – маленькая закругленная верхняя часть коробки – я не уверен, как вы можете управлять индексом – или почему он перегружен, а также наложение порядка сеток не имеет никакого эффекта.

The Old County 27.08.2024 06:16

@TheOldCounty Я обновил код, добавив видео и текстуру, учитывая динамик на экране.

Łukasz D. Mastalerz 27.08.2024 10:01

Это действительно хорошо - спасибо.

The Old County 27.08.2024 12:44

Другие вопросы по теме