// waiv-home.jsx — homepage shell: Direction-B hero + scroll-reveal + nav stick. const { useState: useStateH, useRef: useRefH, useEffect: useEffectH } = React; function HomeHero() { const [finish, setFinish] = useStateH('midnightgold'); const [revealed, setRevealed] = useStateH(false); const tiltRef = useRefH(null); const revealedRef = useRefH(false); revealedRef.current = revealed; // cursor-driven 3D tilt with spring easing useEffectH(() => { const el = tiltRef.current; if (!el) return; const cur = { rx: 7, ry: -23 }, tgt = { rx: 7, ry: -23 }; let raf; const onMove = (e) => { const nx = e.clientX / window.innerWidth - 0.5; const ny = e.clientY / window.innerHeight - 0.5; tgt.ry = (revealedRef.current ? -12 : -23) + nx * 17; tgt.rx = (revealedRef.current ? 5 : 7) - ny * 13; }; const loop = () => { cur.rx += (tgt.rx - cur.rx) * 0.08; cur.ry += (tgt.ry - cur.ry) * 0.08; el.style.transform = `rotateX(${cur.rx.toFixed(2)}deg) rotateY(${cur.ry.toFixed(2)}deg) rotateZ(2deg)`; raf = requestAnimationFrame(loop); }; window.addEventListener('pointermove', onMove); raf = requestAnimationFrame(loop); return () => { window.removeEventListener('pointermove', onMove); cancelAnimationFrame(raf); }; }, []); useEffectH(() => { const ev = new PointerEvent('pointermove', { clientX: window.innerWidth / 2, clientY: window.innerHeight / 2 }); window.dispatchEvent(ev); }, [revealed]); const cfg = WAIV_FINISHES[finish]; return (
); } function Nav() { const [stuck, setStuck] = useStateH(false); const progRef = useRefH(null); useEffectH(() => { const onScroll = () => { setStuck(window.scrollY > 40); const max = document.documentElement.scrollHeight - window.innerHeight; if (progRef.current) progRef.current.style.width = (max > 0 ? (window.scrollY / max) * 100 : 0) + '%'; }; window.addEventListener('scroll', onScroll, { passive: true }); onScroll(); return () => window.removeEventListener('scroll', onScroll); }, []); return ( ); } function App() { // scroll reveal useEffectH(() => { const io = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('is-in'); io.unobserve(e.target); } }); }, { threshold: 0.12, rootMargin: '0px 0px -6% 0px' }); const observeAll = () => document.querySelectorAll('[data-reveal]:not(.is-in)').forEach(el => io.observe(el)); observeAll(); // re-scan for elements mounted later (e.g. gallery Wall, modal content) const mo = new MutationObserver(observeAll); mo.observe(document.body, { childList: true, subtree: true }); return () => { io.disconnect(); mo.disconnect(); }; }, []); // scroll parallax — gentle drift on decorative layers useEffectH(() => { const els = [...document.querySelectorAll('[data-parallax]')].map(el => ({ el, speed: parseFloat(el.getAttribute('data-parallax')) || 0.08, base: el.getAttribute('data-parallax-base') || '', })); if (!els.length || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; let raf; const loop = () => { const h = window.innerHeight; els.forEach(({ el, speed, base }) => { const r = el.getBoundingClientRect(); const rel = (r.top + r.height / 2) - h / 2; el.style.transform = `${base} translate3d(0, ${(-rel * speed).toFixed(1)}px, 0)`.trim(); }); raf = requestAnimationFrame(loop); }; raf = requestAnimationFrame(loop); return () => cancelAnimationFrame(raf); }, []); // magnetic buttons useEffectH(() => { const onMove = (e) => { document.querySelectorAll('.wv-magnetic').forEach(b => { const r = b.getBoundingClientRect(); const dx = e.clientX - (r.left + r.width / 2), dy = e.clientY - (r.top + r.height / 2); const d = Math.hypot(dx, dy), radius = 130; if (d < radius) { const p = (1 - d / radius) * 10; b.style.transform = `translate(${(dx / d) * p || 0}px, ${(dy / d) * p - 2 || -2}px)`; } else b.style.transform = ''; }); }; window.addEventListener('pointermove', onMove); return () => window.removeEventListener('pointermove', onMove); }, []); // custom cursor: lagging ring + dot, reacts to links & draggables useEffectH(() => { if (!window.matchMedia('(hover:hover) and (pointer:fine)').matches) return; document.body.classList.add('wv-cursor-on'); const wrap = document.createElement('div'); wrap.className = 'wv-cur'; const ring = document.createElement('div'); ring.className = 'wv-cur-ring'; const dot = document.createElement('div'); dot.className = 'wv-cur-dot'; wrap.append(ring, dot); document.body.appendChild(wrap); let mx = innerWidth / 2, my = innerHeight / 2, rx = mx, ry = my, raf; const linkSel = 'a, button, .wv-btn, .pg-card, .pg-dot, .so-bubble, .iw-step, .iw-replay, .ol-feat, .hb-finish-btn, .wv-finish-chip, .coll-arrow, [role="button"]'; const onMove = (e) => { mx = e.clientX; my = e.clientY; dot.style.left = mx + 'px'; dot.style.top = my + 'px'; const t = e.target.closest ? e.target.closest(linkSel) : null; const grab = e.target.closest ? e.target.closest('.bud') : null; ring.classList.toggle('is-link', !!t && !grab); dot.classList.toggle('is-link', !!t && !grab); ring.classList.toggle('is-grab', !!grab); }; const onLeave = () => wrap.classList.add('is-hidden'); const onEnter = () => wrap.classList.remove('is-hidden'); const loop = () => { rx += (mx - rx) * 0.18; ry += (my - ry) * 0.18; ring.style.left = rx + 'px'; ring.style.top = ry + 'px'; raf = requestAnimationFrame(loop); }; window.addEventListener('pointermove', onMove); document.addEventListener('mouseleave', onLeave); document.addEventListener('mouseenter', onEnter); raf = requestAnimationFrame(loop); return () => { window.removeEventListener('pointermove', onMove); document.removeEventListener('mouseleave', onLeave); document.removeEventListener('mouseenter', onEnter); cancelAnimationFrame(raf); wrap.remove(); document.body.classList.remove('wv-cursor-on'); }; }, []); return (
); } ReactDOM.createRoot(document.getElementById('root')).render();