reactjsthree.jsreact-three-fiber

How to animate the rectangles using r3f in React?


I'm having a problem to animate the glb model I have in a correct way, but unfortunately I'm not that good with react-three-fiber.

Animation I'm trying to achieve is the background carousel (or I would call it snake effect) which is available at brainsave.ai

reference

I have tried to use Math.sin function to get the similar result, but whatever I do cannot really work with it.

I have some variables:

    const [scrollOffset, setScrollOffset] = useState(0);
    const numRectangles = 20;
    const amplitude = 1; 
    const frequency = 0.4; 
    const spacing = 0.3; 
    const speed = 0.005; 

and using the scroll function I am trying to set the offset:

    const handleScroll = (event) => {
        setScrollOffset((prev) => prev + event.deltaY * speed);
    };

and in my jsx return I'm populating those rectangles and using the x and y coordinates setting the position for the rectangles.

 return (
        <div className={styles.card} onWheel={handleScroll} style={{ height: '100vh', overflow: 'hidden' }}>
            <Canvas>
                <ambientLight intensity={0.5} />
                <spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} intensity={1} />
                <pointLight position={[-10, -10, -10]} intensity={1} />

                {Array.from({ length: numRectangles }).map((_, index) => {
                    // Positioning based on a sine wave pattern
                    const x = -index * spacing;
                    const y = amplitude * Math.sin(frequency * (x - scrollOffset));
                    
                    // Rotation based on the index, creating a gradual rotation
                    const rotation = [4, -10, 0]; // Rotate slightly around the z-axis

                    return <Model key={index} position={[x, y, 0]} rotation={rotation} />;
                })}
            </Canvas>
        </div>
    );

I'm also attaching the codesandbox I'm working on, so you could run the project.

However, animation is not really what I want to achieve, and cannot really get it done. Would appreciate if anyone could help with it.

Happy coding!


Solution

  • Maybe start from this starting point, and adapt to that from the project. Tweak values in Card, then you will find the optimal setting.

    Take a look at this and this and this too.

    export default function Card() {
      const [scrollOffset, setScrollOffset] = useState(0);
      const numRectangles = 60;
      const radius = 2;
      const height = 7;
      //const turns = 5;
      const speed = 0.005;
    
      const handleScroll = (event) => {
        setScrollOffset((prev) => prev + event.deltaY * speed);
      };
    
      return (
        <div
          className={styles.card}
          onWheel={handleScroll}
          style={{ height: "100vh", overflow: "hidden" }}
        >
          <Canvas>
            <ambientLight intensity={0.5} />
            <spotLight
              position={[10, 10, 10]}
              angle={0.15}
              penumbra={1}
              intensity={1}
            />
            <pointLight position={[-10, -10, -10]} intensity={1} />
    
            {Array.from({ length: numRectangles }).map((_, index) => {
              const angle = index * 0.2;
              const x = radius * Math.cos(angle);
              const z = radius * Math.sin(angle);
              const y = (index / numRectangles) * height - height / 2;
              const rotation = [0, 0, 0];
    
              return (
                <Model
                  key={index}
                  position={[x, y - scrollOffset, z]}
                  rotation={rotation}
                />
              );
            })}
          </Canvas>
        </div>
      );
    }
    

    If you want to add animation along a path, you can use progress instead angle.

    <Canvas>
                    <ambientLight intensity={0.5} />
                    <spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} intensity={1} />
                    <pointLight position={[-10, -10, -10]} intensity={1} />
    
                    {Array.from({ length: numRectangles }).map((_, index) => {
                        const progress = (index + scrollOffset) * 0.2; // 
                        const x = radius * Math.cos(progress);         
                        const z = radius * Math.sin(progress);       
                        const y = (index / numRectangles) * height - height / 2;
    
                        const rotation = [0, 0, 0];
    
                        return <Model key={index} position={[x, y, z]} rotation={rotation} />;
                    })}
    </Canvas>
    

    UPDATE

    To give the impression of emerging from the depth (or let's call it from the z-axis), add zDepth. To add smooth scrolling you can use the lerp function to interpolation. You can also simply use the Drei ScrollControl helper.

    import React, { useRef, useState, useEffect } from "react";
    import { Canvas, useFrame } from "@react-three/fiber";
    import styles from "./styles.module.css";
    import { useGLTF, Html } from "@react-three/drei";
    
    // function RedRectangleHelper() {
    //   const rectangleStyle = {
    //     width: "450px",
    //     height: "250px",
    //     border: "2px solid red",
    //     position: "absolute",
    //   };
    
    //   return (
    //     <Html position={[-3.5, 1.5, 0]}>
    //       <div style={rectangleStyle}></div>
    //     </Html>
    //   );
    // }
    
    function Model({ position }) {
      const { nodes, materials } = useGLTF("/timeline-rect.glb");
      return (
        <group position={position} dispose={null}>
          <mesh
            geometry={nodes.Plane_1.geometry}
            material={materials["Material.002"]}
          />
          <mesh
            geometry={nodes.Plane_2.geometry}
            material={materials["Material.001"]}
          />
        </group>
      );
    }
    
    function ScrollingPlanes({ scrollOffset }) {
      const numRectangles = 80;
      const radius = 6;
      const height = 3;
    
      return (
        <>
          {Array.from({ length: numRectangles }).map((_, index) => {
            const progress = (index + scrollOffset) * 0.11;
    
            const x = -radius * Math.sin(progress);
            const z = -radius * Math.sin(progress);
            const y = (index / numRectangles) * height - height / 2;
    
            const zDepth = -index * 0.5;
    
            const rotation = [0, 0, 0];
    
            return (
              <Model
                key={index}
                position={[x, y, z + zDepth]}
                rotation={rotation}
              />
            );
          })}
        </>
      );
    }
    
    function ScrollSmooth({ setScrollOffset }) {
      const targetScrollOffset = useRef(0);
    
      useFrame(() => {
        setScrollOffset((prev) => {
          const lerp = (a, b, t) => a + (b - a) * t;
          return lerp(prev, targetScrollOffset.current, 0.1);
        });
      });
    
      const handleScroll = (event) => {
        targetScrollOffset.current += -event.deltaY * 0.005; //invert delta to creating direction of moving planes
      };
    
      useEffect(() => {
        window.addEventListener("wheel", handleScroll, { passive: true });
        return () => window.removeEventListener("wheel", handleScroll);
      }, []);
    
      return null;
    }
    
    export default function Card() {
      const [scrollOffset, setScrollOffset] = useState(0);
    
      return (
        <div
          className={styles.card}
          style={{ height: "100vh", overflow: "hidden" }}
        >
          <Canvas>
            <ambientLight intensity={0.5} />
            <spotLight
              position={[10, 10, 10]}
              angle={0.15}
              penumbra={1}
              intensity={1}
            />
            <pointLight position={[-10, -10, -10]} intensity={1} />
    
            <ScrollSmooth setScrollOffset={setScrollOffset} />
            <ScrollingPlanes scrollOffset={scrollOffset} />
          </Canvas>
        </div>
      );
    }
    

    SANDBOX