// ShaderAnimation — Three.js fullscreen fragment-shader background. // Ported from the shadcn/TS demo to this project's setup: // • Three.js via CDN global (window.THREE), no `import * as THREE` // • container-sized (ResizeObserver) instead of `w-full h-screen` + window // • fragment shader rewritten to MONOCHROME — white ripple lines on a // transparent canvas (alpha = intensity) so it composites over the // black page and matches the B&W editorial system. Original was RGB. // // Usage: inside a positioned ancestor. const { useEffect: useE_sa, useRef: useR_sa } = React; function ShaderAnimation({ opacity = 0.4, colored = false, style = {} }) { const containerRef = useR_sa(null); useE_sa(() => { const THREE = window.THREE; const container = containerRef.current; if (!THREE || !container) return; const vertexShader = ` void main() { gl_Position = vec4( position, 1.0 ); } `; // Monochrome: accumulate a single intensity, output white with alpha = c. const fragmentMono = ` precision highp float; uniform vec2 resolution; uniform float time; void main(void) { vec2 uv = (gl_FragCoord.xy * 2.0 - resolution.xy) / min(resolution.x, resolution.y); float t = time * 0.05; float lineWidth = 0.0022; float c = 0.0; for(int i = 0; i < 5; i++){ c += lineWidth * float(i*i) / abs(fract(t + float(i)*0.01) * 5.0 - length(uv) + mod(uv.x + uv.y, 0.2)); } c = clamp(c, 0.0, 1.0); gl_FragColor = vec4(vec3(1.0), c); } `; // Colored: per-channel chromatic offset (the original effect). Alpha is the // brightest channel so dark areas stay transparent over the black page. const fragmentColor = ` precision highp float; uniform vec2 resolution; uniform float time; void main(void) { vec2 uv = (gl_FragCoord.xy * 2.0 - resolution.xy) / min(resolution.x, resolution.y); float t = time * 0.05; float lineWidth = 0.0022; vec3 color = vec3(0.0); for(int j = 0; j < 3; j++){ for(int i = 0; i < 5; i++){ color[j] += lineWidth * float(i*i) / abs(fract(t - 0.01*float(j) + float(i)*0.01) * 5.0 - length(uv) + mod(uv.x + uv.y, 0.2)); } } color = clamp(color, 0.0, 1.0); float a = clamp(max(color.r, max(color.g, color.b)), 0.0, 1.0); gl_FragColor = vec4(color, a); } `; const camera = new THREE.Camera(); camera.position.z = 1; const scene = new THREE.Scene(); const geometry = new THREE.PlaneGeometry(2, 2); const uniforms = { time: { value: 1.0 }, resolution: { value: new THREE.Vector2() }, }; const material = new THREE.ShaderMaterial({ uniforms, vertexShader, fragmentShader: colored ? fragmentColor : fragmentMono, transparent: true, }); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh); const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.setClearColor(0x000000, 0); container.appendChild(renderer.domElement); renderer.domElement.style.display = "block"; const onResize = () => { const width = container.clientWidth || window.innerWidth; const height = container.clientHeight || window.innerHeight; renderer.setSize(width, height); uniforms.resolution.value.x = renderer.domElement.width; uniforms.resolution.value.y = renderer.domElement.height; }; onResize(); const ro = new ResizeObserver(onResize); ro.observe(container); window.addEventListener("resize", onResize); let animationId; const animate = () => { animationId = requestAnimationFrame(animate); uniforms.time.value += 0.05; renderer.render(scene, camera); }; animate(); return () => { ro.disconnect(); window.removeEventListener("resize", onResize); cancelAnimationFrame(animationId); geometry.dispose(); material.dispose(); renderer.dispose(); if (renderer.domElement && renderer.domElement.parentNode === container) { container.removeChild(renderer.domElement); } }; }, [opacity, colored]); return (