/* Aurora background — Three.js fragment shader adapted to lime/green palette */ function AuroraBackground({ opacity = 0.55 }) { const ref = React.useRef(null); React.useEffect(() => { const container = ref.current; if (!container || !window.THREE) return; const THREE = window.THREE; const scene = new THREE.Scene(); const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, premultipliedAlpha: false }); renderer.setClearColor(0x000000, 0); const material = new THREE.ShaderMaterial({ uniforms: { iTime: { value: 0 }, iResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, }, transparent: true, vertexShader: ` void main() { gl_Position = vec4(position, 1.0); } `, fragmentShader: ` uniform float iTime; uniform vec2 iResolution; #define NUM_OCTAVES 3 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); float res = mix( mix(rand(ip), rand(ip + vec2(1.0, 0.0)), u.x), mix(rand(ip + vec2(0.0, 1.0)), rand(ip + vec2(1.0, 1.0)), u.x), u.y); return res * res; } float fbm(vec2 x) { float v = 0.0; float a = 0.3; vec2 shift = vec2(100); mat2 rot = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.5)); for (int i = 0; i < NUM_OCTAVES; ++i) { v += a * noise(x); x = rot * x * 2.0 + shift; a *= 0.4; } return v; } void main() { vec2 shake = vec2(sin(iTime * 1.2) * 0.005, cos(iTime * 2.1) * 0.005); vec2 p = ((gl_FragCoord.xy + shake * iResolution.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 < 35.0; i++) { v = p + cos(i * i + (iTime + p.x * 0.08) * 0.025 + i * vec2(13.0, 11.0)) * 3.5 + vec2(sin(iTime * 3.0 + i) * 0.003, cos(iTime * 3.5 - i) * 0.003); float tailNoise = fbm(v + vec2(iTime * 0.5, i)) * 0.3 * (1.0 - (i / 35.0)); /* Lime / forest-green aurora palette — high G, low B, mid-low R */ vec4 auroraColors = vec4( 0.40 + 0.35 * sin(i * 0.2 + iTime * 0.4), /* R: 0.05 → 0.75 */ 0.80 + 0.20 * cos(i * 0.3 + iTime * 0.5), /* G: 0.60 → 1.00 */ 0.08 + 0.12 * sin(i * 0.4 + iTime * 0.3), /* B: -0.04 → 0.20 */ 1.0 ); vec4 currentContribution = auroraColors * exp(sin(i * i + iTime * 0.8)) / length(max(v, vec2(v.x * f * 0.015, v.y * 1.5))); float thinnessFactor = smoothstep(0.0, 1.0, i / 35.0) * 0.6; o += currentContribution * (1.0 + tailNoise * 0.8) * thinnessFactor; } o = tanh(pow(o / 100.0, vec4(1.6))); vec3 col = (o.rgb) * 1.6; float lum = max(max(col.r, col.g), col.b); gl_FragColor = vec4(col, clamp(lum * 1.2, 0.0, 1.0)); } `, }); const mesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material); scene.add(mesh); const setSize = () => { const w = window.innerWidth; const h = window.innerHeight; renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.5)); renderer.setSize(w, h); material.uniforms.iResolution.value.set(w, h); }; setSize(); container.appendChild(renderer.domElement); let frame; let last = performance.now(); const tick = (now) => { const dt = Math.min(0.05, (now - last) / 1000); last = now; material.uniforms.iTime.value += dt; renderer.render(scene, camera); frame = requestAnimationFrame(tick); }; frame = requestAnimationFrame(tick); window.addEventListener('resize', setSize); return () => { cancelAnimationFrame(frame); window.removeEventListener('resize', setSize); if (renderer.domElement && container.contains(renderer.domElement)) { container.removeChild(renderer.domElement); } mesh.geometry.dispose(); material.dispose(); renderer.dispose(); }; }, []); return (