2.9 C
New York
Tuesday, February 17, 2026

Reactive Depth: Constructing a Scroll-Pushed 3D Picture Tube with React Three Fiber



On this tutorial, you’ll discover ways to construct a scroll-driven, infinitely looping 3D picture tube utilizing React Three Fiber. We’ll mix shader-based deformation, inertial movement, deterministic looping, and synchronized DOM overlays to create a tactile and bodily coherent WebGL expertise.

1. Introduction

On this tutorial, we’re going to construct an interactive 3D scene made from three essential components:

  • A grid within the background that reacts to your mouse
  • A cylindrical tube of pictures that scrolls up and down
  • A glass helmet that rotates with the tube

On high of that, we’ll add:

  • A hover impact that lightly slows every little thing down
  • A tooltip constructed within the DOM that follows the mouse
  • A easy customized cursor

The objective isn’t realism. It’s about making a scene the place every little thing feels related.
Scrolling, shifting the mouse, hovering — all of them affect the identical movement system.

2. Movement as a Shared Sign

As a substitute of treating every interplay individually, we let every little thing have an effect on the identical system.

  • Scroll strikes the tube vertically
  • Scroll velocity provides rotation
  • The mouse place adjustments the form of the grid
  • Hover slows down time

All of the essential values stay inside useRef:

const tubeScrollTarget = useRef(0);
const tubeSpinVelocity = useRef(0);
const tubeAngle = useRef(0);
const rotationSpeedScaleTargetRef = useRef(1);

Inside useFrame, we replace every little thing each body:

useFrame((_state, dt) => {
  scrollCurrent.present += 
    (scrollTargetRef.present - scrollCurrent.present) * 0.12;

  spinVelocityRef.present *= Math.pow(0.92, dt * 60);

  rotationSpeedScale.present +=
    (rotationSpeedScaleTargetRef.present - rotationSpeedScale.present) *
    rotationSpeedScaleLerpRef.present;

  const scaledDt = dt * rotationSpeedScale.present;

  angle.present += 
    (baseSpeedRef.present + spinVelocityRef.present) * scaledDt;

  tubeAngleRef.present = angle.present;
});

We don’t use React state right here. Nothing re-renders each body. Every thing stays contained in the animation loop.

3. The Grid Aircraft: Deforming Geometry within the Vertex Shader

The grid is only a aircraft, nevertheless it has quite a lot of subdivisions:

<planeGeometry args={[18, 18, 512, 512]} />

We want many segments as a result of we’re shifting the vertices within the shader.

Right here’s the vertex shader:

various vec2 vUv;

uniform float uEdgeWidth;
uniform float uEdgeAmp;
uniform float uCenterRadius;
uniform float uCenterAmp;
uniform vec2 uCenter;

void essential() {
  vUv = uv;
  vec3 p = place;

  float dEdge = min(
    min(vUv.x, 1.0 - vUv.x),
    min(vUv.y, 1.0 - vUv.y)
  );

  float edgeMask = 1.0 - smoothstep(0.0, uEdgeWidth, dEdge);

  float dCenter = distance(vUv, uCenter);
  float centerMask = 1.0 - smoothstep(0.0, uCenterRadius, dCenter);

  float zOffset = edgeMask * uEdgeAmp
                + centerMask * uCenterAmp;

  p.z += zOffset;

  gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
}

Right here’s what occurs in easy phrases:

  • We measure how shut every vertex is to the sting of the aircraft
  • We measure how shut it’s to the mouse place
  • We use smoothstep to make each results fade easily
  • We push the vertex ahead in Z

There are not any arduous edges, no sudden jumps. Every thing blends easily.

4. Drawing the Grid within the Fragment Shader

The grid itself isn’t a texture. It’s generated mathematically.

First, we animate it over time:

vec2 uv = (vUv + vec2(uTime * uScrollSpeed, 0.0)) * uGridScale;

Then we outline a operate that pulls a line:

float gridLine(float coord, float width) {
  float fw = fwidth(coord);
  float p = abs(fract(coord - 0.5) - 0.5);
  return 1.0 - smoothstep(width * fw, (width + 1.0) * fw, p);
}

The important thing concepts:

  • fract() repeats a worth between 0 and 1, so the sample tiles infinitely
  • The abs(fract(x - 0.5) - 0.5) trick offers us distance from the middle of every cell
  • fwidth() makes the traces anti-aliased and steady at any decision

Full fragment logic:

float gx = gridLine(uv.x, uLineWidth);
float gy = gridLine(uv.y, uLineWidth);
float g = max(gx, gy);

vec3 base = vec3(0.0);
vec3 line = vec3(0.1);

gl_FragColor = vec4(combine(base, line, g), 1.0);

With out fwidth, the traces would shimmer whereas shifting.

5. Seamless Vertical Looping

The tube isn’t infinite. We simply reposition it when wanted.

if (scrollCurrent.present > loopHeight / 2) {
  scrollCurrent.present -= loopHeight;
  scrollTargetRef.present -= loopHeight;
}

We regulate each the present place and the goal worth. That’s what prevents seen jumps. Every picture is positioned round a circle:

const theta = ((col + rowOffset) / cols) * Math.PI * 2;

const x = Math.cos(theta) * radius;
const z = Math.sin(theta) * radius;
const ry = -(theta + Math.PI / 2);

Every aircraft faces outward from the middle.

6. Inertia and Damping

Scroll doesn’t immediately rotate the tube. It provides velocity.

tubeSpinVelocity.present += occasion.deltaY * 0.004;

Each body, we damp it:

spinVelocityRef.present *= Math.pow(0.92, dt * 60);

And clamp it:

spinVelocityRef.present = Math.max(
  -2.0,
  Math.min(2.0, spinVelocityRef.present)
);

That’s what offers us easy, managed movement as a substitute of chaos.

7. Hover Slows Down Time

If you hover a picture, we don’t change rotation immediately. We decelerate time.

rotationSpeedScaleTargetRef.present = 0.35;

Contained in the loop:

rotationSpeedScale.present +=
  (rotationSpeedScaleTargetRef.present - rotationSpeedScale.present) *
  rotationSpeedScaleLerpRef.present;

const scaledDt = dt * rotationSpeedScale.present;

As a result of we scale dt, the entire system slows down constantly. The inertia nonetheless is sensible.

8. Controlling Occasion Propagation

Every mesh stops occasion effervescent:

onPointerOver={(e) => {
  e.stopPropagation();
  onHoverStart(projectName, e);
}}

This prevents hover occasions from interfering with the container-level pointer monitoring.

10. Efficiency

  • No raycasting
  • No React state contained in the animation loop
  • No per-frame allocations
  • Shader-driven deformation
  • DOM animations dealt with exterior React

The body fee stays steady even with sturdy scroll enter.

Wrapping Up

This isn’t only a assortment of animations. It’s one related movement system.

Scroll provides vitality. Power creates rotation. Hover slows time. The shader reshapes area. The DOM reacts to interplay.

Make certain to take a look at all variations:



Supply hyperlink

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles