/* VE GAMERZ — 3D hero: glassy fresnel emblem + particle field + bloom. Degrades to the styled fallback on reduced-motion or no-WebGL/three. */ const { useReducedMotion } = window.VG; function hasWebGL() { try { const c = document.createElement('canvas'); return !!(window.WebGLRenderingContext && (c.getContext('webgl') || c.getContext('experimental-webgl'))); } catch (e) { return false; } } function Hero3D() { const rm = useReducedMotion(); const mount = React.useRef(null); const logoRef = React.useRef(null); const ok = React.useRef(typeof window.THREE !== 'undefined' && hasWebGL()); React.useEffect(() => { if (rm || !ok.current) return; const THREE = window.THREE; const el = mount.current; let W = el.clientWidth, H = el.clientHeight || 600; const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(42, W / H, 0.1, 100); camera.position.set(0, 0, 6.2); const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, powerPreference: 'high-performance', preserveDrawingBuffer: true }); renderer.setSize(W, H); renderer.setPixelRatio(Math.min(devicePixelRatio || 1, 2)); renderer.setClearColor(0x000000, 0); el.appendChild(renderer.domElement); renderer.domElement.style.cssText = 'width:100%;height:100%;display:block;touch-action:pan-y;cursor:grab'; const group = new THREE.Group(); scene.add(group); /* Fresnel glass crystal */ const mat = new THREE.ShaderMaterial({ transparent: true, blending: THREE.AdditiveBlending, depthWrite: false, uniforms: { uEdge: { value: new THREE.Color(0x00e5ff) }, uCore: { value: new THREE.Color(0x032430) }, uPower: { value: 2.4 }, uOpacity: { value: 0.9 } }, vertexShader: 'varying float vF; uniform float uPower; void main(){ vec4 mv=modelViewMatrix*vec4(position,1.); vec3 n=normalize(normalMatrix*normal); vec3 vd=normalize(-mv.xyz); vF=pow(1.0-abs(dot(n,vd)),uPower); gl_Position=projectionMatrix*mv; }', fragmentShader: 'varying float vF; uniform vec3 uEdge; uniform vec3 uCore; uniform float uOpacity; void main(){ vec3 c=mix(uCore,uEdge,clamp(vF,0.0,1.0)); gl_FragColor=vec4(c,(0.18+vF)*uOpacity); }' }); const core = new THREE.Mesh(new THREE.IcosahedronGeometry(1.7, 1), mat); group.add(core); const wire = new THREE.LineSegments( new THREE.EdgesGeometry(new THREE.IcosahedronGeometry(1.72, 1)), new THREE.LineBasicMaterial({ color: 0x00e5ff, transparent: true, opacity: 0.5 }) ); group.add(wire); const innerWire = new THREE.LineSegments( new THREE.EdgesGeometry(new THREE.IcosahedronGeometry(1.05, 0)), new THREE.LineBasicMaterial({ color: 0x66f7ff, transparent: true, opacity: 0.85 }) ); group.add(innerWire); /* Particle field in a shell */ const N = 520, pos = new Float32Array(N * 3); for (let i = 0; i < N; i++) { const r = 2.4 + Math.random() * 3.2, t = Math.random() * Math.PI * 2, p = Math.acos(2 * Math.random() - 1); pos[i * 3] = r * Math.sin(p) * Math.cos(t); pos[i * 3 + 1] = r * Math.sin(p) * Math.sin(t); pos[i * 3 + 2] = r * Math.cos(p); } const pg = new THREE.BufferGeometry(); pg.setAttribute('position', new THREE.BufferAttribute(pos, 3)); const particles = new THREE.Points(pg, new THREE.PointsMaterial({ color: 0x00e5ff, size: 0.045, transparent: true, opacity: 0.85, blending: THREE.AdditiveBlending, depthWrite: false })); scene.add(particles); scene.add(new THREE.PointLight(0x00e5ff, 1.2, 30)); /* Your logo sits in the hollow core as a crisp DOM overlay (logoRef), positioned from the crystal's projected screen center in layout() — reliable load + always centered. */ /* Optional bloom — skipped safely if any post-processing dep is missing */ let composer = null; const canBloom = THREE.EffectComposer && THREE.RenderPass && THREE.UnrealBloomPass; if (canBloom) { try { composer = new THREE.EffectComposer(renderer); composer.addPass(new THREE.RenderPass(scene, camera)); const bloom = new THREE.UnrealBloomPass(new THREE.Vector2(W, H), 0.85, 0.55, 0.15); composer.addPass(bloom); composer.setSize(W, H); composer.setPixelRatio(Math.min(devicePixelRatio || 1, 2)); } catch (e) { composer = null; } } /* Size + place the emblem from the camera's visible frustum, centered in the actually-visible viewport (the hero can be taller than the screen) so it's never clipped by the fold, and kept well right of the headline. */ const BASE_R = 1.72; const layout = () => { const halfH = Math.tan((camera.fov * Math.PI / 180) / 2) * camera.position.z; const halfW = halfH * camera.aspect; const big = W > 800; const targetR = halfH * (big ? 0.34 : 0.42); group.scale.setScalar(targetR / BASE_R); group.position.x = big ? halfW * 0.52 : 0; // ~76% across on desktop const visible = Math.min(H, (window.innerHeight || H) - 72); // minus sticky nav const centerFrac = (72 + visible / 2) / H; // where visible-center sits in the canvas group.position.y = big ? (0.5 - centerFrac) * 2 * halfH : halfH * 0.1; particles.position.x = 0; // pin the logo overlay into the crystal core const elLogo = logoRef.current; if (elLogo) { const cw = el.clientWidth, ch = el.clientHeight; const px = (group.position.x / (2 * halfW) + 0.5) * cw; const py = (0.5 - group.position.y / (2 * halfH)) * ch; const sizePx = (targetR * ch / halfH) * 0.46; // ~46% of crystal diameter elLogo.style.left = px + 'px'; elLogo.style.top = py + 'px'; elLogo.style.width = sizePx + 'px'; elLogo.style.height = sizePx + 'px'; elLogo.style.opacity = '1'; } }; layout(); /* Drag to rotate + inertia + idle auto-spin */ let tRotX = 0.1, tRotY = 0, rotX = 0.1, rotY = 0, vX = 0, vY = 0; let dragging = false, lx = 0, ly = 0, idle = 0; const cvs = renderer.domElement; const down = (e) => { dragging = true; idle = 0; lx = e.clientX; ly = e.clientY; cvs.style.cursor = 'grabbing'; cvs.setPointerCapture && cvs.setPointerCapture(e.pointerId); }; const move = (e) => { if (!dragging) return; idle = 0; vY = (e.clientX - lx) * 0.006; vX = (e.clientY - ly) * 0.006; tRotY += vY; tRotX += vX; lx = e.clientX; ly = e.clientY; }; // ambient follow: sphere tilts toward the cursor from anywhere in the hero const ambient = (e) => { if (dragging) return; idle = 0; const r = cvs.getBoundingClientRect(); const nx = ((e.clientX - r.left) / r.width) * 2 - 1; const ny = ((e.clientY - r.top) / r.height) * 2 - 1; tRotY = nx * 0.6; tRotX = 0.1 + ny * 0.42; }; window.addEventListener('pointermove', ambient, { passive: true }); const up = (e) => { dragging = false; cvs.style.cursor = 'grab'; cvs.releasePointerCapture && cvs.releasePointerCapture(e.pointerId); }; cvs.addEventListener('pointerdown', down); cvs.addEventListener('pointermove', move); cvs.addEventListener('pointerup', up); cvs.addEventListener('pointercancel', up); cvs.addEventListener('pointerleave', () => { dragging = false; }); let raf, t0 = performance.now(), stopped = false; const loop = (now) => { if (stopped) return; raf = requestAnimationFrame(loop); const dt = Math.min(0.05, (now - t0) / 1000); t0 = now; idle += dt; if (!dragging && idle > 1.2) tRotY += dt * 0.28; // idle auto-spin rotX += (tRotX - rotX) * 0.08; rotY += (tRotY - rotY) * 0.08; group.rotation.x = rotX; group.rotation.y = rotY; core.rotation.y -= dt * 0.15; innerWire.rotation.x += dt * 0.4; innerWire.rotation.y += dt * 0.3; particles.rotation.y += dt * 0.03; particles.rotation.x += dt * 0.01; composer ? composer.render() : renderer.render(scene, camera); }; raf = requestAnimationFrame(loop); const onResize = () => { W = el.clientWidth; H = el.clientHeight || 600; camera.aspect = W / H; camera.updateProjectionMatrix(); renderer.setSize(W, H); composer && composer.setSize(W, H); layout(); }; window.addEventListener('resize', onResize); return () => { stopped = true; cancelAnimationFrame(raf); window.removeEventListener('resize', onResize); window.removeEventListener('pointermove', ambient); cvs.removeEventListener('pointerdown', down); cvs.removeEventListener('pointermove', move); cvs.removeEventListener('pointerup', up); cvs.removeEventListener('pointercancel', up); pg.dispose(); mat.dispose(); core.geometry.dispose(); wire.geometry.dispose(); innerWire.geometry.dispose(); renderer.dispose(); if (cvs.parentNode) cvs.parentNode.removeChild(cvs); }; }, [rm]); if (rm || !ok.current) return React.createElement(window.VGHeroFallback); return ( {React.createElement(window.VGHeroFallback, { showEmblem: false })}