Infinite Tunnel

Dynamic Shader-Based Infinite Tunnel Component

The Next Generation

Installation


Install dependencies

npm i three

Copy the CSS Markup

Copy the JSX Source Code

components/infinite-tunnel/infinitetunnel.jsx
'use client'
import { useEffect, useRef } from 'react';
import * as THREE from 'three';

const frag = `
vec4 abyssColor = vec4(0.0, 0.0, 0.3, 1.0);
vec4 tunnelColor = vec4(0.5, 0.7, 1.0, 1.0);

uniform float time;
uniform vec2 resolution;

void main() {
    vec2 uv = (gl_FragCoord.xy - 0.5 * resolution.xy) / resolution.y * 0.6;
    
    float r = length(uv);
    float y = fract(r / 0.005 / (r - 0.01) + time * 1.0);
    
    y = smoothstep(0.01, 4.0, y);
    
    float x = length(uv);
    x = smoothstep(0.5, 0.01, x);
    
    gl_FragColor = mix(tunnelColor, abyssColor, x) * y;
}
`;

const InfiniteTunnel = ({ text }) => {
    const mountRef = useRef(null);
    const requestRef = useRef(null);
    const startTime = useRef(Date.now());
    const renderer = useRef(null);

    useEffect(() => {
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(75, 1, 1, 2);
        camera.position.z = 1;

        const geometry = new THREE.PlaneGeometry(10, 10);
        const material = new THREE.ShaderMaterial({
            uniforms: {
                time: { type: 'f', value: 1.0 },
                resolution: { type: "v2", value: new THREE.Vector2() }
            },
            fragmentShader: frag
        });

        const mesh = new THREE.Mesh(geometry, material);
        scene.add(mesh);

        renderer.current = new THREE.WebGLRenderer({ antialias: true });
        mountRef.current.appendChild(renderer.current.domElement);

        const handleResize = () => {
            const width = mountRef.current.clientWidth;
            const height = mountRef.current.clientHeight;
            camera.aspect = width / height;
            camera.updateProjectionMatrix();
            material.uniforms.resolution.value.set(width, height);
            renderer.current.setSize(width, height);
        };

        handleResize();

        const animate = () => {
            requestRef.current = requestAnimationFrame(animate);
            const elapsedMilliseconds = Date.now() - startTime.current;
            material.uniforms.time.value = elapsedMilliseconds / 1000.0;
            renderer.current.render(scene, camera);
        };

        animate();
        window.addEventListener('resize', handleResize);

        return () => {
            window.removeEventListener('resize', handleResize);
            cancelAnimationFrame(requestRef.current);
            if (mountRef.current ===  null) {null}
            else {mountRef.current.removeChild(renderer.current.domElement)};
        };
    }, []);

    return (
        <div className='relative w-full h-full '>
            <div className='absolute w-max left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-3xl md:text-6xl font-bold '>
                {text}
            </div>
            <div className='absolute' ref={mountRef} style={{ width: '100%', height: '100%' }} />
        </div>
    );
};

export default InfiniteTunnel;