Я создал эту модель Threejs в 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>
</>
);
}
Я хочу, чтобы он вращался при прокрутке пользователя, как в этой демонстрации липкого ящика. 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>
Я не знаю, почему это так - у меня код работает.
Я пытаюсь понять вашу проблему, но мне сложно понять эту гифку, а код, который вы вставили, только меняет ваше вращение. Я думаю, попробуйте создать новую песочницу и вставить URL-адрес или уточнить ожидания от тура при редактировании сообщения.
Ну еще нужно добавить видео/изображение - videos.ctfassets.net/f1onadsih6xk/1SswaYYtFZR6dTOsFMurFW/… или/и videos.ctfassets.net/f1onadsih6xk/1v3hNplvIPRuHnIW7qpIpd/…
Позже я понял, о чем вы говорили, проверьте меня, пожалуйста.





Вы можете сделать это несколькими способами. Вы можете использовать какой-нибудь фрейм, чтобы разместить холст как часть прокрутки 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 - как бы вы изменили это, чтобы в телефоне было изображение/видео - чтобы все сложилось вместе - у нас есть создать новую сетку внутри группы?
Я не уверен в этом — codeandbox.io/p/sandbox/…
Я не знаю - я только что изменил ротацию - я помещу код в тело сообщения
Я видел на справочном сайте, который просматривал - его загрузка в видео из тега img --- <img src = "videos.ctfassets.net/f1onadsih6xk/5xdqjOZgZHvYos0cTRB4D6/…" alt = "Document видео за секунды" loading = "lazy">
Да, но я бы предложил localhost mp4 - тогда как встраивание на YouTube - потому что размер, скорее всего, соответствует размеру мобильного устройства - если бы это была модель ноутбука, широкоэкранный YouTube мог бы подойти - хорошо бы выставить его как встраивание, отличное от YouTube. - просто используя mp4 или изображение
Мне удалось использовать тег видео – я заметил в вашей вставке на YouTube и метод тега видео — он фактически закрывает динамики телефона – маленькая закругленная верхняя часть коробки – я не уверен, как вы можете управлять индексом – или почему он перегружен, а также наложение порядка сеток не имеет никакого эффекта.
@TheOldCounty Я обновил код, добавив видео и текстуру, учитывая динамик на экране.
Это действительно хорошо - спасибо.
Первая песочница не работает.