// ─────────────────────────────────────────────── // Shader background — fixed canvas behind everything // Three.js fragment shader (aurora) tinted to teal accent // ─────────────────────────────────────────────── const SHADER_VERT = `void main(){gl_Position=vec4(position,1.0);}`; const SHADER_FRAG = `precision mediump float; uniform float iTime; uniform vec2 iResolution; float rand(vec2 n){return fract(sin(dot(n,vec2(12.9898,4.1414)))*43758.5453);} float noise(vec2 p){ vec2 ip=floor(p); vec2 u=fract(p); u=u*u*(3.0-2.0*u); return mix(mix(rand(ip),rand(ip+vec2(1.,0.)),u.x),mix(rand(ip+vec2(0.,1.)),rand(ip+vec2(1.,1.)),u.x),u.y); } float fbm(vec2 x){ float v=0.0; float a=0.3; mat2 rot=mat2(cos(0.5),sin(0.5),-sin(0.5),cos(0.5)); for(int i=0;i<3;i++){ v+=a*noise(x); x=rot*x*2.0+vec2(100.0); a*=0.4; } return v; } void main(){ vec2 p=((gl_FragCoord.xy)-iResolution.xy*0.5)/iResolution.y*mat2(6.0,-4.0,4.0,6.0); vec2 v; vec4 o=vec4(0.0); float f=2.0+fbm(p+vec2(iTime*5.0,0.0))*0.5; for(float i=0.0;i<22.0;i++){ v=p+cos(i*i+(iTime+p.x*0.08)*0.025+i*vec2(13.0,11.0))*3.5; float tn=fbm(v+vec2(iTime*0.5,i))*0.25*(1.0-(i/22.0)); vec4 c=vec4( 0.05+0.08*sin(i*0.2+iTime*0.4), 0.55+0.35*cos(i*0.3+iTime*0.5), 0.55+0.30*sin(i*0.4+iTime*0.3), 1.0); vec4 cc=c*exp(sin(i*i+iTime*0.8))/length(max(v,vec2(v.x*f*0.015,v.y*1.5))); float t=smoothstep(0.0,1.0,i/22.0)*0.6; o+=cc*(1.0+tn*0.8)*t; } o = tanh(pow(o / 60.0, vec4(1.4))); vec3 c = o.rgb * 2.5; // alpha = brightness so dark areas stay transparent float a = clamp(max(max(c.r, c.g), c.b), 0.0, 1.0); gl_FragColor = vec4(c, a); }`; function ShaderBackground() { const ref = React.useRef(null); React.useEffect(() => { if (!ref.current) return; const THREE = window.THREE; if (!THREE) { console.warn('[shader-bg] THREE not loaded'); return; } const container = ref.current; let renderer, mesh, geometry, material, raf = 0; let running = true; try { const w = Math.max(1, window.innerWidth); const h = Math.max(1, window.innerHeight); renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true, powerPreference: 'low-power' }); renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.25)); renderer.setSize(w, h, false); const dom = renderer.domElement; dom.style.display = 'block'; dom.style.width = '100%'; dom.style.height = '100%'; container.appendChild(dom); const scene = new THREE.Scene(); const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); const uniforms = { iTime: { value: 0 }, iResolution: { value: new THREE.Vector2(w, h) } }; material = new THREE.ShaderMaterial({ uniforms, vertexShader: SHADER_VERT, fragmentShader: SHADER_FRAG, transparent: true }); geometry = new THREE.PlaneGeometry(2, 2); mesh = new THREE.Mesh(geometry, material); scene.add(mesh); let last = performance.now(); const tick = (now) => { if (!running) return; const dt = Math.min(0.05, (now - last) / 1000); last = now; uniforms.iTime.value += dt; try { renderer.render(scene, camera); } catch (e) { console.warn('[shader-bg] render error', e); running = false; return; } raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); // pause when tab hidden const onVis = () => { if (document.hidden) { running = false; cancelAnimationFrame(raf); } else if (!running) { running = true; last = performance.now(); raf = requestAnimationFrame(tick); } }; document.addEventListener('visibilitychange', onVis); // throttled resize let resizeT = 0; const onResize = () => { clearTimeout(resizeT); resizeT = setTimeout(() => { const nw = Math.max(1, window.innerWidth); const nh = Math.max(1, window.innerHeight); renderer.setSize(nw, nh, false); uniforms.iResolution.value.set(nw, nh); }, 150); }; window.addEventListener('resize', onResize); return () => { running = false; cancelAnimationFrame(raf); clearTimeout(resizeT); document.removeEventListener('visibilitychange', onVis); window.removeEventListener('resize', onResize); if (dom.parentNode) dom.parentNode.removeChild(dom); geometry && geometry.dispose(); material && material.dispose(); renderer && renderer.dispose(); }; } catch (e) { console.warn('[shader-bg] init failed', e); running = false; if (renderer) { try { renderer.dispose(); } catch(_){} } } }, []); return
; } window.ShaderBackground = ShaderBackground;