/* ZaviFx cinematic hero — dark stage, kinetic editorial headline, 3D brand sphere. Intentional dark component (like the footer / ink product card), not a dark theme. */ const { Button, Badge } = window.ZaviFxDesignSystem_f15c31; const ROTATING = ['systems.', 'engines.', 'websites.', 'brands.']; /* ---------- scroll-reveal helper used by the whole page ---------- */ function useReveal() { React.useEffect(() => { if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { document.querySelectorAll('[data-reveal]').forEach((el) => el.setAttribute('data-shown', '')); return; } const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) { e.target.setAttribute('data-shown', ''); io.unobserve(e.target); } }); }, { rootMargin: '0px 0px -12% 0px', threshold: 0.08 }); const scan = () => document.querySelectorAll('[data-reveal]:not([data-shown])').forEach((el) => io.observe(el)); scan(); const t = setInterval(scan, 600); return () => { clearInterval(t); io.disconnect(); }; }, []); } /* ---------- three.js sphere, hero-scale ---------- */ function HeroSphere3D({ intensity = 1 }) { 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 < 720; const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(36, w / h, 0.1, 100); camera.position.set(0, 0.2, 6.4); 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 = 8; const ball = new THREE.Mesh( new THREE.SphereGeometry(1.5, mobile ? 56 : 128, mobile ? 36 : 80), new THREE.MeshStandardMaterial({ map: tex, emissiveMap: tex, emissive: new THREE.Color('#ffffff'), emissiveIntensity: 0.3, roughness: 0.22, metalness: 0.1 }) ); scene.add(ball); const halo = new THREE.Mesh( new THREE.SphereGeometry(1.95, 48, 32), new THREE.MeshBasicMaterial({ color: new THREE.Color('#FF7A2F'), transparent: true, opacity: 0.07 }) ); scene.add(halo); /* orbiting particle field — subtle, not decorative noise */ const N = mobile ? 240 : 620; const pos = new Float32Array(N * 3); for (let i = 0; i < N; i++) { const r = 2.3 + Math.random() * 2.6, a = Math.random() * Math.PI * 2, b = (Math.random() - 0.5) * 1.1; pos[i * 3] = Math.cos(a) * r; pos[i * 3 + 1] = b * r * 0.42; pos[i * 3 + 2] = Math.sin(a) * r; } const pg = new THREE.BufferGeometry(); pg.setAttribute('position', new THREE.BufferAttribute(pos, 3)); const dust = new THREE.Points(pg, new THREE.PointsMaterial({ color: 0xffb277, size: 0.018, transparent: true, opacity: 0.5, sizeAttenuation: true })); scene.add(dust); scene.add(new THREE.HemisphereLight(0xffffff, 0x552200, 1.0)); const key = new THREE.DirectionalLight(0xffffff, 2.4); key.position.set(3, 3.5, 4); scene.add(key); const rim = new THREE.PointLight(0xffb066, 4.0, 16); rim.position.set(-3.6, -1.4, 2.4); scene.add(rim); const cool = new THREE.PointLight(0x7fa8ff, 1.6, 16); cool.position.set(2.8, -2.4, -2.6); scene.add(cool); 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) * 0.6; ty = ((e.clientY - r.top) / r.height - 0.5) * 0.34; }; 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.0055; px += (tx - px) * 0.045; py += (ty - py) * 0.045; const s = Math.min(scroll / 900, 1); ball.rotation.y = t * 1.35; ball.rotation.z = -0.2 + Math.sin(t * 0.55) * 0.05; ball.position.x = Math.sin(t * 0.45) * 0.22 + px * 1.5; ball.position.y = Math.sin(t * 0.95) * 0.05 - py - s * 0.9; ball.scale.setScalar(1 - s * 0.12); halo.position.copy(ball.position); halo.scale.setScalar(1 + Math.sin(t * 1.6) * 0.012); dust.rotation.y = -t * 0.42; dust.rotation.x = 0.16 + py * 0.3; dust.position.y = -s * 0.5; camera.position.x = px * 0.55; camera.lookAt(0, -s * 0.4, 0); 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); ball.geometry.dispose(); ball.material.dispose(); tex.dispose(); halo.geometry.dispose(); halo.material.dispose(); pg.dispose(); dust.material.dispose(); renderer.dispose(); if (renderer.domElement.parentNode === el) el.removeChild(renderer.domElement); }; }, [reduce]); const orbRef = React.useRef(null); const onOrbMove = (e) => { const el = orbRef.current || e.currentTarget; orbRef.current = el; const r = el.getBoundingClientRect(); el.style.setProperty('--mx', (((e.clientX - r.left) / r.width) * 100).toFixed(2) + '%'); el.style.setProperty('--my', (((e.clientY - r.top) / r.height) * 100).toFixed(2) + '%'); el.dataset.reveal = ''; }; const onOrbLeave = (e) => { delete e.currentTarget.dataset.reveal; }; return (
); } /* ---------- rotating word ---------- */ function WordSwap() { const [i, setI] = React.useState(0); React.useEffect(() => { if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; const id = setInterval(() => setI((n) => (n + 1) % ROTATING.length), 2400); return () => clearInterval(id); }, []); return ( websites. {ROTATING.map((w, n) => ( {w} ))} ); } function HeroCinematic({ go }) { const stage = React.useRef(null); const [tick, setTick] = React.useState(0); React.useEffect(() => { if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; const id = setInterval(() => setTick((n) => n + 1), 3200); return () => clearInterval(id); }, []); const trail = (e) => { const el = stage.current; if (!el) return; const r = el.getBoundingClientRect(); el.style.setProperty('--cx', (((e.clientX - r.left) / r.width) * 100).toFixed(1) + '%'); el.style.setProperty('--cy', (((e.clientY - r.top) / r.height) * 100).toFixed(1) + '%'); }; const proof = [['37+', 'websites shipped'], ['12', 'disciplines in-house'], ['48h', 'proposal turnaround'], ['1', 'point of contact']]; return (
Strategy, design, engineering and performance marketing. One team, one brief, building growth