"use client"; import { useEffect, useRef } from "react"; import { useReducedMotion } from "motion/react"; type InfiniteTunnelProps = { /** Six-digit hex colour of the tunnel lamps. */ lamp?: string; /** 1 is the default pace; 2 is twice as fast. */ speed?: number; /** Name read out by screen readers. */ label?: string; /** Set the size here. */ className?: string; }; const MAX_PIXELS = 900000; const VERTEX = `attribute vec2 p;void main(){gl_Position=vec4(p,0.,1.);}`; const FRAGMENT = `precision highp float; uniform vec2 res,look; uniform vec3 lampC; uniform float z0,time,rush; const float GAP=3.2,LANE=-.68; vec2 path(float z){return vec2(sin(z*.085)*2.2+sin(z*.037+1.)*1.4,sin(z*.06)*.35);} float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);} float tube(vec3 p){ vec2 q=p.xy-path(p.z); return min((1.85-length(q*vec2(.78,1.)-vec2(0.,.35)))*.8,q.y+1.); } float march(vec3 o,vec3 d,int count){ float t=.05; for(int i=0;i<44;i++){ if(i>=count)break; float h=tube(o+d*t); if(h<.0025*t||t>64.)break; t+=h; } return t; } vec3 glowAt(vec3 p,vec3 n){ float k=floor(p.z/GAP); vec3 sum=vec3(0.); for(int i=0;i<3;i++){ float z=(k+float(i)-.5)*GAP; vec2 c=path(z); for(int j=0;j<2;j++){ vec3 L=vec3(c+vec2(j==0?-1.15:1.15,1.8),z)-p; float r=dot(L,L); sum+=max(dot(n,L)/sqrt(r),0.)/(1.+r*1.3); } } return sum; } vec3 tail(vec3 o,vec3 d,float t){ float z=z0+11.+sin(time*.3)*2.-rush*5.; vec3 sum=vec3(0.); for(int i=0;i<2;i++){ vec3 c=vec3(path(z)+vec2(LANE+(i==0?-.3:.3),-.56),z)-o; float along=dot(c,d); if(along>0.&&along1.4){ float tube2=smoothstep(.15,.11,abs(abs(q.x)-1.15))*smoothstep(.6+rush*1.+.05,.6+rush*1.,abs(mid)); lit+=lampC*tube2*6.; } if(mod(k,7.)<.5&&abs(q.y-.45)<.12&&abs(q.x)>1.){ lit=mix(lit,vec3(.06,1.,.42)*1.4,smoothstep(.2,.18,abs(mid))*smoothstep(.09,.08,abs(q.y-.45))); } return lit; } void main(){ vec2 uv=(gl_FragCoord.xy-.5*res)/(.5*min(res.x,res.y)); vec3 o=vec3(path(z0)+vec2(LANE,-.42),z0); vec3 aim=vec3(path(z0+6.)+vec2(LANE,-.2),z0+6.); aim.xy+=look*vec2(1.5,.7); vec3 f=normalize(aim-o),r=normalize(cross(vec3(0.,1.,0.),f)),u=cross(f,r); float roll=(path(z0+7.).x-path(z0).x)*.035,cr=cos(roll),sr=sin(roll); uv=vec2(cr*uv.x-sr*uv.y,sr*uv.x+cr*uv.y); vec3 d=normalize(uv.x*r+uv.y*u+1.3*f); vec3 fog=lampC*lampC*.12; float t=march(o,d,44); vec3 p=o+d*t,n; float wet; vec3 c=surface(p,d,t,true,n,wet); if(wet>0.){ float f2=.04+.96*pow(1.-max(dot(-d,n),0.),5.); float g=sin(p.z*7.+sin(p.x*5.))*sin(p.x*9.); vec3 b=normalize(reflect(d,n)+vec3(g*.012,0.,g*.03)); vec3 from=p+n*.012; float t2=march(from,b,30); vec3 n2;float w2; vec3 back=surface(from+b*t2,b,t2,false,n2,w2); back=mix(back,fog,1.-exp(-t2*.05)); back*=smoothstep(.12,.8,dot(back,vec3(.33))); back+=tail(from,b,t2); c+=back*(f2*.7+.32)*wet; } c=mix(c,fog,1.-exp(-t*.06)); c+=tail(o,d,t); c*=1.-dot(uv,uv)*.12; c=1.-exp(-c*1.35); gl_FragColor=vec4(pow(c,vec3(.8)),1.); }`; const rgb = (hex: string) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255); function program(gl: WebGLRenderingContext) { const made = gl.createProgram(); for (const [type, text] of [[gl.VERTEX_SHADER, VERTEX], [gl.FRAGMENT_SHADER, FRAGMENT]] as const) { const shader = gl.createShader(type); if (!made || !shader) return null; gl.shaderSource(shader, text); gl.compileShader(shader); gl.attachShader(made, shader); } if (!made) return null; gl.linkProgram(made); return gl.getProgramParameter(made, gl.LINK_STATUS) ? made : null; } /* InfiniteTunnel: a night drive through a road tunnel that never ends. Tiled walls, lamp strips, a wet road, tail lights ahead. The mouse looks around; hold a press, Space or Enter to speed up. Runs only while on screen. */ export function InfiniteTunnel({ lamp = "#ff9a3c", speed = 1, label = "A night drive through an endless road tunnel. Hold to go faster.", className = "aspect-[16/9] w-[28rem]", }: InfiniteTunnelProps) { const reduce = useReducedMotion(); const boxRef = useRef(null); const plainRef = useRef(null); useEffect(() => { const box = boxRef.current; const plain = plainRef.current; if (!box || !plain) return; const canvas = document.createElement("canvas"); canvas.setAttribute("aria-hidden", "true"); canvas.style.cssText = "position:absolute;inset:0;width:100%;height:100%"; const gl = canvas.getContext("webgl", { alpha: false, antialias: false }); const made = gl && program(gl); if (!gl || !made) return; gl.useProgram(made); gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW); const corner = gl.getAttribLocation(made, "p"); gl.enableVertexAttribArray(corner); gl.vertexAttribPointer(corner, 2, gl.FLOAT, false, 0, 0); const at = (name: string) => gl.getUniformLocation(made, name); gl.uniform3fv(at("lampC"), rgb(lamp)); let raf = 0; let last = 0; let inView = false; let time = 0; let z = 44; let rush = 0; let lookX = 0; let lookY = 0; let goalX = 0; let goalY = 0; let holding = false; const draw = () => { gl.uniform2f(at("look"), lookX, lookY); gl.uniform1f(at("z0"), z); gl.uniform1f(at("time"), time % 1000); gl.uniform1f(at("rush"), rush); gl.drawArrays(gl.TRIANGLES, 0, 3); }; const frame = (now: number) => { const dt = Math.min(0.05, (now - last) / 1000 || 0); last = now; time += dt; rush += ((holding ? 1 : 0) - rush) * Math.min(1, dt * 3); z += dt * speed * (8 + rush * 30); lookX += (goalX - lookX) * Math.min(1, dt * 4); lookY += (goalY - lookY) * Math.min(1, dt * 4); draw(); raf = inView ? requestAnimationFrame(frame) : 0; }; const start = () => { if (inView && !raf && !reduce) raf = requestAnimationFrame(frame); }; const fit = () => { const { width, height } = box.getBoundingClientRect(); if (!width || !height) return; const scale = Math.min(Math.max(window.devicePixelRatio, 1.5), 2, Math.sqrt(MAX_PIXELS / (width * height))); canvas.width = Math.round(width * scale); canvas.height = Math.round(height * scale); gl.viewport(0, 0, canvas.width, canvas.height); gl.uniform2f(at("res"), canvas.width, canvas.height); draw(); }; const hold = () => { if (reduce) { z += 7; draw(); } else holding = true; }; const free = () => { holding = false; }; const point = (e: PointerEvent) => { if (e.pointerType !== "mouse") return; const r = box.getBoundingClientRect(); goalX = ((e.clientX - r.left) / r.width - 0.5) * 2; goalY = (0.5 - (e.clientY - r.top) / r.height) * 2; if (reduce) { lookX = goalX; lookY = goalY; draw(); } }; const leave = () => { goalX = 0; goalY = 0; holding = false; }; const keyDown = (e: KeyboardEvent) => { if (e.key !== " " && e.key !== "Enter") return; e.preventDefault(); if (!e.repeat) hold(); }; const keyUp = (e: KeyboardEvent) => { if (e.key === " " || e.key === "Enter") free(); }; fit(); box.append(canvas); plain.hidden = true; const sized = new ResizeObserver(fit); sized.observe(box); const watch = new IntersectionObserver(([entry]) => { inView = entry.isIntersecting; start(); }); watch.observe(box); const lost = () => { canvas.remove(); plain.hidden = false; }; canvas.addEventListener("webglcontextlost", lost); box.addEventListener("pointerdown", hold); box.addEventListener("pointerup", free); box.addEventListener("pointercancel", free); box.addEventListener("pointermove", point); box.addEventListener("pointerleave", leave); box.addEventListener("blur", free); box.addEventListener("keydown", keyDown); box.addEventListener("keyup", keyUp); return () => { cancelAnimationFrame(raf); sized.disconnect(); watch.disconnect(); canvas.removeEventListener("webglcontextlost", lost); box.removeEventListener("pointerdown", hold); box.removeEventListener("pointerup", free); box.removeEventListener("pointercancel", free); box.removeEventListener("pointermove", point); box.removeEventListener("pointerleave", leave); box.removeEventListener("blur", free); box.removeEventListener("keydown", keyDown); box.removeEventListener("keyup", keyUp); gl.getExtension("WEBGL_lose_context")?.loseContext(); canvas.remove(); plain.hidden = false; }; }, [lamp, speed, reduce]); return ( ); }