// 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 (
);
}
window.ShaderAnimation = ShaderAnimation;