React-Three/Fiber Vertical Sticky Carousel for Mobile Devices using Three.js

https://i.sstatic.net/Z5F8K0mS.gif

After stacking the sticky areas, I faced a challenge in adding text in the same style. Additionally, I created refs for each phone scene but the stickiness was lost. I am uncertain about what caused it to stick initially and how to control the duration each part is visible to the user.

The result is multiple phones rendered, however, the sticky aspect has been compromised.

https://codesandbox.io/p/sandbox/r3f-scroll-rig-sticky-box-forked-3zpm73

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 = (ref) => {
  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: ref[0],
        scrub: 1,
        markers: true,
        pin: true,
        start: 'top top',
        end: 'bottom top'
      }
    })

    tl.to(group.current.rotation, { z: -Math.PI / 8, duration: 2 })
  }, [materials.Screen])

  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 = () => {
  const threeSceneGroup = useRef()

  return (
    <div id="three-canvas-container" style={{ width: '100vw', height: '500px' }}>
      <div>
        <h2>header text</h2>
        <p>text text text</p>
      </div>
      <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 ref={threeSceneGroup} />
        <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 />
    <ThreeScene />
    <ThreeScene />
    <ThreeScene />
    <ThreeScene />

    <TextSection />
  </div>
)

export default App

This version renders only one phone but maintains working sticky features.

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


Struggling with fixing the image orientation, this vector2 function came in handy

const imageTexture = new THREE.TextureLoader().load(image)
    
imageTexture.wrapS = imageTexture.wrapT = THREE.RepeatWrapping
imageTexture.anisotropy = 16

imageTexture.repeat = new THREE.Vector2(1, -1)

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

Answer №1

Consider attempting something similar to this approach. You might be wondering in your next query about how to create animations for displaying and hiding text based on each section. Simply implement opacity: 0=>1=>0 at a specific point in the viewport using ScrollTrigger.

import React, { useRef, useEffect } from 'react';
import { Canvas, useThree } from '@react-three/fiber';
import { useGLTF, OrbitControls } from '@react-three/drei';
import * as THREE from 'three';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

gsap.registerPlugin(ScrollTrigger);

const IphoneModel = ({ modelIndex }) => {
  const group = useRef();
  const { nodes, materials } = useGLTF('/Iphone15.glb');

  useEffect(() => {
    const imageTexture = new THREE.TextureLoader().load(
      'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/lava/lavatile.jpg'
    );
    materials.Screen.map = imageTexture;
    materials.Screen.needsUpdate = true;

    const tl = gsap.timeline({
      scrollTrigger: {
        trigger: `#section-${modelIndex}`,
        scrub: 1,
        pin: true,
        start: 'top top',
        end: 'bottom top',
        markers: true
      }
    });

    tl.to(group.current.rotation, { z: Math.PI * 2, duration: 2 });
  }, [materials.Screen, modelIndex]);

  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 ThreeScene = ({ modelIndex }) => (
  <div
    id={`three-canvas-container-${modelIndex}`}
    style={{ position: 'absolute', top: 0, right: 0, width: '50vw', height: '100vh' }}
  >
    <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 modelIndex={modelIndex} />
      <OrbitControls enableZoom={false} />
      <Background />
    </Canvas>
  </div>
);

const Section = ({ title, content, modelIndex }) => (
  <div id={`section-${modelIndex}`} style={{ display: 'flex', alignItems: 'center', height: '100vh', position: 'relative' }}>
    <div style={{ width: '50vw', padding: '0 2rem' }}>
      <h1>{title}</h1>
      <p dangerouslySetInnerHTML={{ __html: content }} />
    </div>
    <ThreeScene modelIndex={modelIndex} />
  </div>
);

const App = () => (
  <div style={{ height: '300vh' }}>
    <Section
      modelIndex={1}
      title="First Section"
      content="Lorem Ipsum is simply dummy text of<br />the printing and typesetting industry."
    />
    <Section
      modelIndex={2}
      title="Second Section"
      content="Lorem Ipsum is simply dummy text of<br />the printing and typesetting industry."
    />
    <Section
      modelIndex={3}
      title="Third Section"
      content="Lorem Ipsum is simply dummy text of<br />the printing and typesetting industry."
    />
  </div>
);

export default App;

Answer №2

I have made progress, but I am still uncertain about the factors influencing the sticky behavior and why the last image is not loading across all devices.

https://i.sstatic.net/YdZGFCx7.png

Visit Code Sandbox for More Details

import React, { useRef, useEffect } from 'react'
...

--

It seems to be an issue with screen cloning, as the first item appears blank.

Further exploration on Code Sandbox here

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

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

materials.Screen.needsUpdate = true

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Having difficulties executing a JavaScript file in the command prompt

I'm having trouble running a JavaScript file in the command prompt. Can anyone assist me with this issue? D:\>Node Welcome to Node.js v12.14.1. Type ".help" for more information. > 001.js undefined > Node 001.js Thrown: Node 001.js ...

Tips for disabling rotation of a glTF model in three.js

I need some assistance with creating a jewelry AR try-on experience using Jeeliz Facetracking and Three.js. The face tracking and model tracking are functioning correctly, but I am facing an issue where the gltf model/scene rotates based on head rotation ...

NextJS - Accessing local files with readdir and readFile functions leads to error: Module not found when trying to resolve 'fs' module

I have been working with NextJS and have successfully used the getStaticProps functionality in previous projects to populate data. However, I recently set up a new NextJS project and it seems that this functionality is no longer working. When starting th ...

Incorporate a lightbox within a repeater field in advanced custom fields

I have developed a gallery with dynamic pills using advanced custom fields and I need to add a lightbox to it. I've tried several times to add the code for the lightbox but all my attempts have been unsuccessful. I have already added all the necessar ...

Utilize AJAX to insert information into a MySQL database when a checkbox is selected

Before I got stuck here, I took a look at how a similar question was implemented here I attempted to implement the code in order to insert data into a MySQL database when a checkbox is clicked. While it may have been easier to do this on form submission, ...

Is it possible to leverage the flex features of react-native in a manner similar to the row/column system of

Is it a good idea to utilize flex in react native by creating custom components that retrieve flex values? I previously used the bootstrap grid system and now I am exploring react native. Is there a way to implement this example using react-native bootstr ...

Using JavaScript/jQuery to implement a mouseover effect with a delay

I'm struggling with a code that reveals hidden divs after a slight delay on mouseover. The issue I'm facing is that I lack expertise in CS, and within that code, there are elements as follows: $(document).ready(function() { var timer; var ...

Can you provide a way to directly calculate the total length of all arrays within an array of objects?

How can I find the total length of arrays in an array of objects based on a specific property? var myArray = [{ "a" : 1, "b" : another Array }, { "c" : 2, "b" : another Array } ..... ] Is there a more efficient way to achieve this instea ...

Leverage async and await features in TypeScript aiming at ES5 compatibility

Currently working on a TypeScript project that is set to target ES5, I am exploring the feasibility of incorporating async/await functionality. Syntactically, the TypeScript compiler is able to transpile the code without issues. However, it has come to my ...

What factors determine when Angular automatically triggers a setTimeout function compared to another?

Sorry if this all seems a bit odd, but I'll do my best to explain the situation. We have been given access to a small service that provides a simple form UI which we collect results from using an event. We've successfully implemented this in two ...

The variable ReactFauxDOM has not been declared

Exploring the combination of D3 and React components. Utilizing OliverCaldwell's Faux-DOM element has led me to encounter a frustrating error message stating "ReactFauxDOM is not defined”. Despite following the npm install process correctly... It s ...

Ways to transfer a state from the child component to the app component

I have 2 different components that contain sub-components within them. In one of these components, I'm trying to figure out how to transfer the click event from the sub-component to another component so that it can render an entirely new component for ...

What is the origin of the "has" in the "this.has" syntax?

While going through the Nats-Jetstream documentation, I stumbled upon this interesting code snippet: var createModel = require('jetstream').model; var Shape = createModel('Shape', function() { this.has('x', Number); t ...

Utilized OBJLoader to load an item successfully; now seeking guidance on calculating the coordinates of an element within (three.js)

I am utilizing three.js and OBJLoader to create a 3D human head representation: let renderer, camera, scene, head, light, projectiles; new THREE.OBJLoader().load(objUrl, initialize); function initialize(obj) { renderer = new THREE.WebGLRenderer({ al ...

Updating a table in Javascript after deleting a specific row

Is there a way to automatically reindex rows in a table after deleting a row? For example, if I delete row 1, can the remaining rows be reordered so that they are numbered sequentially? function reindexRows(tableID) { try { var t ...

What is the best way to retrieve a collection of DOM elements in JSX?

I'm in need of rendering certain components only if a specific condition is met. To achieve this, I have written the following inside the render() method: return ( <div className={classes.root}> {registrationScreen && ( * ...

Guide on dynamically assigning the value of an Angular variable to another variable

My website has a slideshow feature with left and right buttons, similar to this example: . I am using Angular to change the image when the left or right button is clicked. In the function, I am incrementing a value: /*SlideShow Pictures*/ $scope.pic ...

Dealing with Large JSON Strings in ASP.NET MVC Views

Large JSON Objects (approximately 1.5 MB) are received in Controller C1. They are then converted to strings and stored in a hidden label in View V1. The JSON data in V1 is utilized by parsing it in JavaScript J1. An occurrence of Out of Memory Excepti ...

Attempting to decode the string prior to selecting an item from the Vue.js/Nuxt array

Hey there, I really appreciate anyone who can assist me with this. I've been dabbling in Laravel for a few months and now I'm trying to dive into studying Nuxt. The specific type of translation I need help with is proving to be quite challenging ...

Searching for data in Node.js using Mongoose and dates

I'm in search of a way to execute a specific query with mongoose. In my mongodb database, I have data structured like this: "startDateTime" : ISODate("2017-03-22T00:00:00.000Z"), "endDateTime" : ISODate("2017-03-27T00:00:00.000Z"), My goal is to r ...