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