/* Rolling ZaviFx sphere. three.js when available + motion allowed; static brand fallback otherwise. */ function HeroSphere({ height = 460 }) { const mount = React.useRef(null); const [failed, setFailed] = React.useState(false); const reduce = typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches; React.useEffect(() => { if (reduce || !window.THREE || !mount.current) { if (!window.THREE) setFailed(true); return; } const THREE = window.THREE; const el = mount.current; const w = el.clientWidth, h = el.clientHeight; const mobile = w < 640; const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(38, w / h, 0.1, 100); camera.position.set(0, 0.35, 5.2); const renderer = new THREE.WebGLRenderer({ antialias: !mobile, alpha: true, powerPreference: 'high-performance' }); renderer.setPixelRatio(Math.min(window.devicePixelRatio, mobile ? 1.5 : 2)); renderer.setSize(w, h); el.appendChild(renderer.domElement); const tex = new THREE.TextureLoader().load(window.ASSETS + 'sphere-zavifx-texture.png'); tex.anisotropy = 4; const geo = new THREE.SphereGeometry(1.35, mobile ? 48 : 96, mobile ? 32 : 64); const mat = new THREE.MeshStandardMaterial({ map: tex, roughness: 0.24, metalness: 0.12, emissiveMap: tex, emissive: new THREE.Color('#FFFFFF'), emissiveIntensity: 0.22 }); const ball = new THREE.Mesh(geo, mat); scene.add(ball); const halo = new THREE.Mesh( new THREE.SphereGeometry(1.62, 48, 32), new THREE.MeshBasicMaterial({ color: new THREE.Color('#FF7A2F'), transparent: true, opacity: 0.09 }) ); scene.add(halo); scene.add(new THREE.HemisphereLight(0xffffff, 0x884422, 1.15)); const key = new THREE.DirectionalLight(0xffffff, 2.6); key.position.set(3, 4, 4); scene.add(key); const fill = new THREE.DirectionalLight(0xfff0e0, 1.1); fill.position.set(-2, 1.5, 3.5); scene.add(fill); const rim = new THREE.PointLight(0xffc48a, 3.2, 14); rim.position.set(-3.4, -1.2, 2.6); scene.add(rim); const cool = new THREE.PointLight(0x88aaff, 1.3, 14); cool.position.set(2.6, -2.2, -2.4); scene.add(cool); let raf = 0, t = 0, px = 0, py = 0, tx = 0, ty = 0, running = true; const onPointer = (e) => { const r = el.getBoundingClientRect(); tx = ((e.clientX - r.left) / r.width - 0.5) * 0.5; ty = ((e.clientY - r.top) / r.height - 0.5) * 0.3; }; if (!mobile) el.addEventListener('pointermove', onPointer); const io = new IntersectionObserver(([entry]) => { running = entry.isIntersecting; }, { threshold: 0.05 }); io.observe(el); const tick = () => { raf = requestAnimationFrame(tick); if (!running) return; t += 0.006; px += (tx - px) * 0.05; py += (ty - py) * 0.05; ball.rotation.y = t * 1.6; ball.rotation.z = -0.22 + Math.sin(t * 0.6) * 0.05; ball.position.x = Math.sin(t * 0.5) * 0.42 + px * 1.6; ball.position.y = 0.06 + Math.sin(t * 1.1) * 0.05 - py; halo.position.copy(ball.position); camera.position.x = px * 0.7; camera.lookAt(0, 0, 0); renderer.render(scene, camera); }; tick(); const onResize = () => { const nw = el.clientWidth, nh = el.clientHeight; camera.aspect = nw / nh; camera.updateProjectionMatrix(); renderer.setSize(nw, nh); }; window.addEventListener('resize', onResize); return () => { cancelAnimationFrame(raf); io.disconnect(); window.removeEventListener('resize', onResize); el.removeEventListener('pointermove', onPointer); geo.dispose(); mat.dispose(); tex.dispose(); halo.geometry.dispose(); halo.material.dispose(); renderer.dispose(); if (renderer.domElement.parentNode === el) el.removeChild(renderer.domElement); }; }, [reduce]); const staticFallback = reduce || failed; return (
); } Object.assign(window, { HeroSphere });