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
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!
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>
);
}