/* Continuously orbiting 3D composition — ceramic plates, arcs, gold pill, dust, guide lines. Choreography modelled on the reference frames; palette is ZaviFx amber → crimson. */ function HeroScene() { const mount = React.useRef(null); const [fallback, setFallback] = React.useState(false); const reduce = typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches; React.useEffect(() => { if (reduce || !window.THREE || !mount.current) { setFallback(true); return; } const THREE = window.THREE, el = mount.current; let w = el.clientWidth, h = el.clientHeight; const mobile = w < 760; const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(38, w / h, 0.1, 120); const renderer = new THREE.WebGLRenderer({ antialias: !mobile, alpha: true, powerPreference: 'high-performance' }); renderer.setPixelRatio(Math.min(window.devicePixelRatio, mobile ? 1.4 : 2)); renderer.setSize(w, h); renderer.shadowMap.enabled = !mobile; renderer.shadowMap.type = THREE.PCFSoftShadowMap; el.appendChild(renderer.domElement); const rig = new THREE.Group(); scene.add(rig); const ceramic = (extra) => new THREE.MeshStandardMaterial(Object.assign({ color: 0xF2EEEA, roughness: 0.62, metalness: 0.02 }, extra || {})); const gold = new THREE.MeshStandardMaterial({ color: 0xF6B93B, roughness: 0.18, metalness: 1, emissive: 0x5a3200, emissiveIntensity: 0.35 }); /* floating plates */ const plates = []; const plateGeo = new THREE.BoxGeometry(3.1, 0.13, 3.1); [[-1.9, -1.15, 0.4, 0.06], [2.15, -0.35, -1.6, -0.10], [0.2, 1.85, -2.4, 0.14]].forEach(([x, y, z, r]) => { const m = new THREE.Mesh(plateGeo, ceramic()); m.position.set(x, y, z); m.rotation.set(r, r * 2.2, r * 0.6); m.castShadow = m.receiveShadow = !mobile; rig.add(m); plates.push(m); }); /* torus arcs — the signature shapes */ const arcs = []; const arcA = new THREE.Mesh(new THREE.TorusGeometry(1.32, 0.2, mobile ? 18 : 34, mobile ? 60 : 120, Math.PI * 1.35), ceramic()); arcA.position.set(-0.15, 0.15, 0.1); arcA.rotation.set(1.32, 0.2, 0.5); arcA.castShadow = !mobile; rig.add(arcA); arcs.push(arcA); const arcB = new THREE.Mesh(new THREE.TorusGeometry(0.86, 0.15, mobile ? 16 : 28, mobile ? 48 : 96, Math.PI * 1.1), ceramic()); arcB.position.set(1.75, -0.9, 0.9); arcB.rotation.set(0.9, -0.5, -0.8); arcB.castShadow = !mobile; rig.add(arcB); arcs.push(arcB); const arcC = new THREE.Mesh(new THREE.TorusGeometry(1.05, 0.13, 16, mobile ? 44 : 88, Math.PI * 0.85), ceramic()); arcC.position.set(-2.1, 0.95, -0.9); arcC.rotation.set(0.4, 0.9, 1.3); rig.add(arcC); arcs.push(arcC); /* brand sphere — the ZaviFx mark, kept as the focal object */ const tex = new THREE.TextureLoader().load(window.ASSETS + 'sphere-zavifx-texture.png'); tex.anisotropy = 8; const orbUniforms = { uMap: { value: tex }, uProgress: { value: 0 }, uPointer: { value: new THREE.Vector2(0, 0) }, uTime: { value: 0 }, }; const orb = new THREE.Mesh( new THREE.SphereGeometry(0.62, mobile ? 48 : 110, mobile ? 32 : 72), new THREE.ShaderMaterial({ uniforms: orbUniforms, transparent: true, vertexShader: [ 'varying vec2 vUv; varying vec3 vN; varying vec3 vV;', 'void main(){ vUv = uv; vN = normalize(normalMatrix * normal);', ' vec4 mv = modelViewMatrix * vec4(position,1.0); vV = normalize(-mv.xyz);', ' gl_Position = projectionMatrix * mv; }' ].join('\n'), fragmentShader: [ 'uniform sampler2D uMap; uniform float uProgress; uniform float uTime; uniform vec2 uPointer;', 'varying vec2 vUv; varying vec3 vN; varying vec3 vV;', 'float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7)))*43758.5453); }', 'float cellNoise(vec2 p){ vec2 i = floor(p); vec2 f = fract(p); float d = 1.0;', ' for(int y=-1;y<=1;y++){ for(int x=-1;x<=1;x++){ vec2 g = vec2(float(x),float(y));', ' vec2 o = vec2(hash(i+g), hash(i+g+7.3)); d = min(d, length(g + o - f)); } } return 1.0 - d; }', 'void main(){', ' vec2 par = uPointer * 0.012;', ' vec4 base = texture2D(uMap, vUv + par);', /* tiled dot field, cell-noise brightness */ ' vec2 tUv = vec2(vUv.x * 2.0, vUv.y);', ' float tiling = 90.0;', ' vec2 tiled = mod(tUv * tiling, 2.0) - 1.0;', ' float bright = cellNoise(tUv * tiling * 0.5);', ' float dist = length(tiled);', ' float dot0 = smoothstep(0.52, 0.46, dist) * bright;', /* scan band travelling across the depth proxy */ ' float depth = vUv.y * 0.75 + (vN.z * 0.5 + 0.5) * 0.25;', ' float flow = 1.0 - smoothstep(0.0, 0.035, abs(depth - uProgress));', ' float band = 1.0 - smoothstep(0.0, 0.10, abs(depth - uProgress));', /* brand-warm mask, screen-blended over the wordmark texture */ ' vec3 hot = vec3(1.00, 0.42, 0.11);', ' vec3 mask = hot * dot0 * flow * 3.2;', ' vec3 col = 1.0 - (1.0 - base.rgb) * (1.0 - clamp(mask, 0.0, 1.0));', ' col += hot * band * 0.12;', /* fresnel rim + gentle breathing glow */ ' float fres = pow(1.0 - max(dot(vN, vV), 0.0), 2.4);', ' col += vec3(1.0, 0.62, 0.28) * fres * 0.55;', ' col += hot * (0.04 + 0.03 * sin(uTime * 1.6));', ' gl_FragColor = vec4(col, 1.0);', '}' ].join('\n'), }) ); orb.position.set(0.05, 0.2, 0.15); orb.castShadow = !mobile; rig.add(orb); const halo = new THREE.Mesh(new THREE.SphereGeometry(0.95, 32, 24), new THREE.MeshBasicMaterial({ color: 0xFF7A2F, transparent: true, opacity: 0.1 })); rig.add(halo); /* gold pills riding the arcs */ const pills = []; for (let i = 0; i < 3; i++) { const p = new THREE.Mesh(new THREE.SphereGeometry(0.115, 24, 18), gold); p.scale.set(1, 0.55, 1); p.castShadow = !mobile; rig.add(p); pills.push(p); } /* thin guide lines */ const lineMat = new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.13 }); for (let i = 0; i < 6; i++) { const g = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(-9, -2.4 + i * 0.95, -5 + i * 1.4), new THREE.Vector3(9, -2.4 + i * 0.95 + (i % 2 ? 1.6 : -1.2), -5 + i * 1.4), ]); rig.add(new THREE.Line(g, lineMat)); } /* dust */ const N = mobile ? 300 : 780; const pos = new Float32Array(N * 3); for (let i = 0; i < N; i++) { pos[i * 3] = (Math.random() - 0.5) * 16; pos[i * 3 + 1] = (Math.random() - 0.5) * 9; pos[i * 3 + 2] = (Math.random() - 0.5) * 12; } const dg = new THREE.BufferGeometry(); dg.setAttribute('position', new THREE.BufferAttribute(pos, 3)); const dust = new THREE.Points(dg, new THREE.PointsMaterial({ color: 0xffc48a, size: 0.026, transparent: true, opacity: 0.55, sizeAttenuation: true })); scene.add(dust); /* studio lighting with warm bleed */ scene.add(new THREE.HemisphereLight(0xfff3e6, 0x2a1206, 0.9)); const key = new THREE.DirectionalLight(0xffffff, 2.6); key.position.set(4, 6, 5); key.castShadow = !mobile; if (!mobile) { key.shadow.mapSize.set(1024, 1024); key.shadow.radius = 6; } scene.add(key); const bleedA = new THREE.PointLight(0xFF6B1F, 7, 16); bleedA.position.set(-3.2, -1.1, 2.2); scene.add(bleedA); const bleedB = new THREE.PointLight(0xFFC24A, 4.5, 14); bleedB.position.set(2.9, 1.6, 1.4); scene.add(bleedB); const bleedC = new THREE.PointLight(0x8FB4FF, 2.2, 14); bleedC.position.set(0.4, -2.6, -3.2); scene.add(bleedC); let raf = 0, t = 0, px = 0, py = 0, tx = 0, ty = 0, live = true, scroll = 0; const onPointer = (e) => { const r = el.getBoundingClientRect(); tx = ((e.clientX - r.left) / r.width - 0.5); ty = ((e.clientY - r.top) / r.height - 0.5); }; if (!mobile) window.addEventListener('pointermove', onPointer); const onScroll = () => { scroll = window.scrollY; }; window.addEventListener('scroll', onScroll, { passive: true }); const io = new IntersectionObserver(([e]) => { live = e.isIntersecting; }, { threshold: 0.02 }); io.observe(el); const tick = () => { raf = requestAnimationFrame(tick); if (!live) return; t += 0.0042; px += (tx - px) * 0.04; py += (ty - py) * 0.04; const s = Math.min(scroll / 1000, 1); /* continuous camera orbit — the reference's defining move */ const rad = 7.4 + Math.sin(t * 0.5) * 0.5; camera.position.set( Math.sin(t * 0.36) * rad * 0.42 + px * 1.5, 1.5 + Math.sin(t * 0.27) * 0.7 - py * 1.1 - s * 1.4, Math.cos(t * 0.36) * rad ); camera.lookAt(0, 0.1 - s * 0.5, 0); rig.rotation.y = t * 0.14; orb.rotation.y = t * 1.5; orbUniforms.uProgress.value = Math.sin(t * 1.9) * 0.5 + 0.5; orbUniforms.uTime.value = t * 60; orbUniforms.uPointer.value.set(px * 4, py * 4); halo.position.copy(orb.position); halo.scale.setScalar(1 + Math.sin(t * 1.8) * 0.02); plates.forEach((m, i) => { m.position.y += Math.sin(t * (0.8 + i * 0.22)) * 0.0016; m.rotation.z += 0.0004 * (i % 2 ? 1 : -1); }); arcs.forEach((a, i) => { a.rotation.x += 0.0016 * (i % 2 ? 1 : -0.7); a.rotation.y += 0.0011; a.position.y += Math.sin(t * (1.1 + i * 0.3)) * 0.0014; }); pills.forEach((p, i) => { const a = arcs[i], ang = t * (0.9 + i * 0.35) + i * 2.1; const R = [1.32, 0.86, 1.05][i]; const v = new THREE.Vector3(Math.cos(ang) * R, Math.sin(ang) * R, 0).applyEuler(a.rotation).add(a.position); p.position.copy(v); p.rotation.y = ang; }); dust.rotation.y = -t * 0.28; dust.position.y = -s * 0.8; renderer.render(scene, camera); }; tick(); const onResize = () => { w = el.clientWidth; h = el.clientHeight; camera.aspect = w / h; camera.updateProjectionMatrix(); renderer.setSize(w, h); }; window.addEventListener('resize', onResize); return () => { cancelAnimationFrame(raf); io.disconnect(); window.removeEventListener('resize', onResize); window.removeEventListener('pointermove', onPointer); window.removeEventListener('scroll', onScroll); scene.traverse((o) => { if (o.geometry) o.geometry.dispose(); if (o.material) (Array.isArray(o.material) ? o.material : [o.material]).forEach((m) => m.dispose()); }); tex.dispose(); renderer.dispose(); if (renderer.domElement.parentNode === el) el.removeChild(renderer.domElement); }; }, [reduce]); return (
); } Object.assign(window, { HeroScene });