/* ============ CHER — Three.js visuals (hero particles + Lightfall) ============ */ const { useEffect, useRef } = React; const prefersReduced = () => window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; /* ---- observe visibility to pause RAF when offscreen ---- */ function useInViewRef(onChange){ const ref = useRef(null); useEffect(()=>{ if(!ref.current) return; const io = new IntersectionObserver(([e])=>onChange(e.isIntersecting),{threshold:0.01}); io.observe(ref.current); return ()=>io.disconnect(); },[]); return ref; } /* ================= GOLD HERO ================= */ function GoldHero(){ const canvasRef = useRef(null); const visibleRef = useRef(true); const wrapRef = useInViewRef(v=>{ visibleRef.current = v; }); useEffect(()=>{ const canvas = canvasRef.current; if(!canvas || !window.THREE) return; const THREE = window.THREE; const reduced = prefersReduced(); const scene = new THREE.Scene(); scene.fog = new THREE.FogExp2(0x0b1d33, 0.0016); const camera = new THREE.PerspectiveCamera(62, innerWidth/innerHeight, 0.1, 2000); camera.position.set(0, 6, 230); const renderer = new THREE.WebGLRenderer({ canvas, antialias:true, alpha:true, powerPreference:'high-performance' }); renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); renderer.setSize(innerWidth, innerHeight); renderer.setClearColor(0x000000, 0); renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 0.9; // ---- gold particle field ---- const palette = [ new THREE.Color(0xE6C79A), new THREE.Color(0xC99A6B), new THREE.Color(0xE6C79A), new THREE.Color(0xd9b487), new THREE.Color(0xF7F5F0), new THREE.Color(0x9fb0b5) ]; const groups = []; const layerConf = [ { count: reduced?260:520, spread: 360, depth:-40, size:[1.2,3.2], speed:0.012 }, { count: reduced?180:360, spread: 520, depth:-260, size:[2.0,5.0], speed:0.007 }, { count: reduced?60:130, spread: 300, depth: 80, size:[3.0,7.5], speed:0.018 } ]; layerConf.forEach((L,li)=>{ const g = new THREE.BufferGeometry(); const pos = new Float32Array(L.count*3); const col = new Float32Array(L.count*3); const siz = new Float32Array(L.count); const pha = new Float32Array(L.count); for(let i=0;i0.5) discard; float core = smoothstep(0.5,0.0,d); float halo = pow(core, 2.2); float a = halo * vTw; gl_FragColor = vec4(vColor, a); }`, transparent:true, blending:THREE.AdditiveBlending, depthWrite:false }); const pts = new THREE.Points(g, mat); pts.userData.speed = L.speed; scene.add(pts); groups.push(pts); }); // soft ambient glow planes (gold + teal bokeh) function glow(color, size, x,y,z, op){ const cv = document.createElement('canvas'); cv.width=cv.height=128; const ctx = cv.getContext('2d'); const gr = ctx.createRadialGradient(64,64,0,64,64,64); gr.addColorStop(0, color); gr.addColorStop(1, 'rgba(0,0,0,0)'); ctx.fillStyle=gr; ctx.fillRect(0,0,128,128); const tex = new THREE.CanvasTexture(cv); const sp = new THREE.Sprite(new THREE.SpriteMaterial({map:tex, transparent:true, opacity:op, blending:THREE.AdditiveBlending, depthWrite:false})); sp.scale.set(size,size,1); sp.position.set(x,y,z); scene.add(sp); return sp; } glow('rgba(201,154,107,0.9)', 360, -160, 40, -120, 0.5); glow('rgba(21,65,74,0.9)', 460, 220, -60, -180, 0.55); glow('rgba(230,199,154,0.8)', 200, 120, 90, 30, 0.35); let raf, t=0; let mx=0, my=0, tmx=0, tmy=0; const onMove = e=>{ tmx=(e.clientX/innerWidth-0.5); tmy=(e.clientY/innerHeight-0.5); }; window.addEventListener('pointermove', onMove); const clock = new THREE.Clock(); function loop(){ raf = requestAnimationFrame(loop); if(!visibleRef.current) return; const dt = Math.min(clock.getDelta(), 0.05); t += dt; mx += (tmx-mx)*0.04; my += (tmy-my)*0.04; groups.forEach((p,i)=>{ p.material.uniforms.time.value = t; p.rotation.z += p.userData.speed*dt*0.5; }); // gentle parallax camera camera.position.x += ((mx*36) - camera.position.x)*0.05 + Math.sin(t*0.1)*0.02; camera.position.y += ((6 - my*26) - camera.position.y)*0.05; camera.lookAt(0,4,-40); renderer.render(scene, camera); } loop(); const onResize = ()=>{ camera.aspect = innerWidth/innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth, innerHeight); }; window.addEventListener('resize', onResize); return ()=>{ cancelAnimationFrame(raf); window.removeEventListener('resize', onResize); window.removeEventListener('pointermove', onMove); groups.forEach(p=>{ p.geometry.dispose(); p.material.dispose(); }); renderer.dispose(); }; },[]); return React.createElement('div', { ref:wrapRef, style:{position:'absolute',inset:0,zIndex:0} }, React.createElement('canvas', { ref:canvasRef, id:'hero-canvas' }) ); } /* ================= LIGHTFALL (brand-tinted) ================= */ const LIGHTFALL_FRAG = ` precision highp float; uniform vec3 iResolution; uniform vec2 iMouse; uniform float iTime; uniform vec3 uC0; uniform vec3 uC1; uniform vec3 uC2; uniform vec3 uBg; uniform float uSpeed; uniform float uOpacity; varying vec2 vUv; vec3 palette(float h){ if(h<0.34) return uC0; if(h<0.67) return uC1; return uC2; } vec3 tanhv(vec3 x){ vec3 e=exp(-2.0*x); return (1.0-e)/(1.0+e); } vec2 sceneC(vec2 frag, vec2 r){ vec2 P=(frag+frag-r)/r.x; float z=0.0; float d=1e3; vec4 O=vec4(0.0); for(int k=0;k<34;k++){ if(d<=1e-4) break; O=z*normalize(vec4(P,3.0,0.0))-vec4(0.0,4.0,1.0,0.0)/4.5; d=1.0-sqrt(length(O*O)); z+=d; } return vec2(O.x, atan(O.z,O.y)); } void main(){ vec2 C=vUv*iResolution.xy; vec2 r=iResolution.xy; vec2 uv0=(C+C-r)/r.x; float T=0.1*iTime*uSpeed+9.0; float angRings=6.0; vec2 Y=vec2(5e-3,6.28318530718/angRings); vec2 c0=sceneC(C,r); vec2 cdx=sceneC(C+vec2(1.,0.),r); vec2 cdy=sceneC(C+vec2(0.,1.),r); vec2 dCx=cdx-c0; vec2 dCy=cdy-c0; dCx.y-=6.28318530718*floor(dCx.y/6.28318530718+0.5); dCy.y-=6.28318530718*floor(dCy.y/6.28318530718+0.5); vec2 fw=abs(dCx)+abs(dCy); C=c0; vec2 P=vec2(2.,1.)*uv0-(r/r.x)*vec2(0.,1.); vec4 O=vec4(uBg*70.0/(1e3*dot(P,P)+6.0),0.0); float zr=5e-4; vec2 rr=vec2(max(length(fw),1e-5)); float tail=19.0; for(int m=0;m<3;m++){ float jf=float(m)+1.0; float ic=fract(sin(dot(vec2(jf,floor(C.x/Y.x+0.5)),vec2(7.,11.))*73.0)); vec2 Pp=C-(T+T*ic)*vec2(0.,1.); Pp-=floor(Pp/Y+0.5)*Y; float h=fract(8663.0*ic); vec3 col=palette(h); float weight=mix(1.5,1.0+sin(T+7.0*h+4.0),1.0); vec2 inner=vec2(length(max(Pp,vec2(-1.,0.))),length(Pp)-zr)-zr; vec2 sm=vec2(1.0)-smoothstep(-rr,rr,inner); O.rgb+=dot(sm,vec2(exp(tail*Pp.y),3.0))*col*weight; C.x+=Y.x/8.0; } vec3 colr=sqrt(tanhv(max(O.rgb*0.85-vec3(0.04,0.08,0.02),0.0))); gl_FragColor=vec4(colr,uOpacity); }`; function hexToVec(THREE,h){ const c=new THREE.Color(h); return new THREE.Vector3(c.r,c.g,c.b); } function Lightfall(){ const canvasRef = useRef(null); const visibleRef = useRef(true); const wrapRef = useInViewRef(v=>{ visibleRef.current=v; }); useEffect(()=>{ const canvas = canvasRef.current; if(!canvas || !window.THREE) return; const THREE = window.THREE; const parent = canvas.parentElement; const renderer = new THREE.WebGLRenderer({ canvas, antialias:true, alpha:true }); renderer.setPixelRatio(Math.min(devicePixelRatio,1.6)); const scene = new THREE.Scene(); const camera = new THREE.OrthographicCamera(-1,1,1,-1,0,1); const uniforms = { iResolution:{value:new THREE.Vector3(1,1,1)}, iMouse:{value:new THREE.Vector2(0,0)}, iTime:{value:0}, uC0:{value:hexToVec(THREE,0xE6C79A)}, uC1:{value:hexToVec(THREE,0xC99A6B)}, uC2:{value:hexToVec(THREE,0x2c7a86)}, uBg:{value:hexToVec(THREE,0x123a5c)}, uSpeed:{value:0.5}, uOpacity:{value:0.85} }; const mat = new THREE.ShaderMaterial({ uniforms, vertexShader:`varying vec2 vUv; void main(){ vUv=uv; gl_Position=vec4(position.xy,0.0,1.0); }`, fragmentShader:LIGHTFALL_FRAG, transparent:true }); const quad = new THREE.Mesh(new THREE.PlaneGeometry(2,2), mat); scene.add(quad); function resize(){ const w = parent.clientWidth, h = parent.clientHeight; renderer.setSize(w,h,false); const dpr = renderer.getPixelRatio(); uniforms.iResolution.value.set(w*dpr, h*dpr, 1); } resize(); const ro = new ResizeObserver(resize); ro.observe(parent); const onMove = e=>{ const rect=canvas.getBoundingClientRect(); uniforms.iMouse.value.set((e.clientX-rect.left)*renderer.getPixelRatio(),(rect.height-(e.clientY-rect.top))*renderer.getPixelRatio()); }; parent.addEventListener('pointermove', onMove); let raf; const reduced = prefersReduced(); function loop(t){ raf = requestAnimationFrame(loop); if(!visibleRef.current) return; uniforms.iTime.value = reduced ? 6.0 : t*0.001; renderer.render(scene,camera); if(reduced){ cancelAnimationFrame(raf); } } raf = requestAnimationFrame(loop); return ()=>{ cancelAnimationFrame(raf); ro.disconnect(); parent.removeEventListener('pointermove',onMove); mat.dispose(); quad.geometry.dispose(); renderer.dispose(); }; },[]); return React.createElement('div',{ref:wrapRef,style:{position:'absolute',inset:0,zIndex:0}}, React.createElement('canvas',{ref:canvasRef,id:'lightfall-canvas'}) ); } window.GoldHero = GoldHero; window.Lightfall = Lightfall; window.prefersReduced = prefersReduced;