// ============================================================ // SECTION 2 — React wrapper for the 3D scroll parallax // Center-top title that animates between "The Product" and // "The Ingredients" as scroll passes the threshold. // ============================================================ // ============================================================ // DissolveText — letter-by-letter burst/dissolve transition. // On text change: each letter of the old text staggers out // (translate + scale + blur + fade), then new letters stagger // in from below with the same dissolve curve. // ============================================================ function DissolveText({ text, duration = 700 }) { const [current, setCurrent] = React.useState(text); const [outgoing, setOutgoing] = React.useState(null); React.useEffect(() => { if (current === text) return; setOutgoing(current); setCurrent(text); const t = setTimeout(() => setOutgoing(null), duration); return () => clearTimeout(t); }, [text]); const renderLetters = (str, cls) => str.split('').map((ch, i) => ( {ch === ' ' ? '\u00A0' : ch} )); return ( {outgoing && ( {renderLetters(outgoing, 's2-dl-out')} )} {renderLetters(current, 's2-dl-in')} ); } function Section2() { const sectionRef = React.useRef(null); const canvasRef = React.useRef(null); const [progress, setProgress] = React.useState(0); const [ready, setReady] = React.useState(false); const [noWebGL, setNoWebGL] = React.useState(false); React.useEffect(() => { const onNoWebGL = () => setNoWebGL(true); window.addEventListener('yojea3d:nowebgl', onNoWebGL); return () => window.removeEventListener('yojea3d:nowebgl', onNoWebGL); }, []); React.useEffect(() => { let raf = 0; let mounted = true; let started = false; const tryInit = async () => { if (!window.__yojea3D || !canvasRef.current) { setTimeout(tryInit, 80); return; } await window.__yojea3D.init(canvasRef.current); if (mounted) setReady(true); }; // ⚡ Defer the ~5 MB GLB download + WebGL init until Section 2 is near // the viewport — the hero/top of page loads fast and the 3D only // fetches when the user actually scrolls toward it. const startInit = () => { if (!started) { started = true; tryInit(); } }; let initObserver = null; if ('IntersectionObserver' in window && sectionRef.current) { initObserver = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) { startInit(); initObserver.disconnect(); } }, { rootMargin: '800px 0px' }); initObserver.observe(sectionRef.current); } else { startInit(); } function onScroll() { if (!sectionRef.current) return; const rect = sectionRef.current.getBoundingClientRect(); const total = rect.height - window.innerHeight; const scrolled = -rect.top; const p = Math.max(0, Math.min(1, scrolled / Math.max(1, total))); cancelAnimationFrame(raf); raf = requestAnimationFrame(() => { setProgress(p); if (window.__yojea3D) window.__yojea3D.setProgress(p); }); } window.addEventListener('scroll', onScroll, { passive: true }); onScroll(); return () => { mounted = false; if (initObserver) initObserver.disconnect(); window.removeEventListener('scroll', onScroll); cancelAnimationFrame(raf); if (window.__yojea3D) window.__yojea3D.dispose(); }; }, []); // 6 ingredients in a hexagon-ish layout around center sachet const INGREDIENTS = [ { key: 'bunga-sidowayah', name: 'Bunga Sidowayah', img: 'assets/bunga-sidowayah.webp', side: 'l', y: 0.10, threshold: 0.82, latin: 'Woodfordia floribunda', desc: 'Mengandung asam elagat, asam galat, flavonoid, dan tanin yang membantu mengurangi nyeri haid, serta meringankan keputihan dan pendarahan berlebih.', tags: ['Nyeri Haid', 'Keputihan', 'Flavonoid'], }, { key: 'kulit-delima', name: 'Kulit Delima', img: 'assets/kulit-delima.webp', side: 'l', y: 0.42, threshold: 0.86, latin: 'Punica granatum', desc: 'Kaya saponin, flavonoid, dan alkaloid yang menghambat keputihan akibat jamur, mengurangi risiko kanker rahim, serta mencegah peradangan dan kerusakan sel.', tags: ['Saponin', 'Keputihan', 'Anti-inflamasi'], }, { key: 'kayu-rapat', name: 'Kayu Rapat', img: 'assets/kayu-rapat.webp', side: 'l', y: 0.72, threshold: 0.90, latin: 'Parameria laevigata', desc: 'Mengandung flavonoid dan polifenol yang membantu mengencangkan otot kewanitaan, mencegah infeksi, melancarkan haid, dan meredakan sakit pasca melahirkan.', tags: ['Kewanitaan', 'Pasca Lahiran', 'Polifenol'], }, { key: 'daun-tapak-liman', name: 'Daun Tapak Liman', img: 'assets/daun-tapak-liman.webp', side: 'r', y: 0.10, threshold: 0.84, latin: 'Elephantopus scaber', desc: 'Mengandung senyawa fenolik sebagai antioksidan, antiinflamasi, dan antimikroba, membantu meredakan nyeri haid, membersihkan darah, serta menghambat sel kanker.', tags: ['Antioksidan', 'Anti-inflamasi', 'Antimikroba'], }, { key: 'pulosari', name: 'Pulosari', img: 'assets/pulosari.webp', side: 'r', y: 0.42, threshold: 0.88, latin: 'Alyxia reinwardtii', desc: 'Mengandung flavonoid, saponin, dan polifenol yang meningkatkan daya tahan tubuh, memperlancar haid, mengurangi keputihan, dan bersifat antioksidan.', tags: ['Imunitas', 'Haid', 'Antioksidan'], }, { key: 'madu', name: 'Madu Murni', img: 'assets/madu.webp', side: 'r', y: 0.72, threshold: 0.92, latin: 'Mel purum', desc: 'Bersifat antibakteri, antioksidan, dan antiinflamasi. Membantu meredakan nyeri haid.', tags: ['Antibakteri', 'Antioksidan', 'Pelembap'], }, ]; const [selected, setSelected] = React.useState(null); React.useEffect(() => { const onKey = (e) => { if (e.key === 'Escape') setSelected(null); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, []); // Title timing — "The Product" appears EARLY and stays locked through // the entire rotation + windup. Switches to "The Ingredients" as dissolve begins. // p ≤ 0.04 → no title // 0.04 – 0.50 → "The Product" (locked, no movement) // p ≥ 0.50 → "The Ingredients" const titleTarget = progress <= 0.04 ? '' : progress < 0.50 ? 'The Product' : 'The Ingredients'; // No parallax during box phase — title stays still. const titleParallax = progress < 0.50 ? 0 : (progress - 0.5) * -14; return ( {/* Decorative background */} {/* CENTER-TOP TITLE — dissolve transition + slight scroll parallax */} {/* 3D canvas */} {!ready && !noWebGL && ( Memuat 3D Yojea… )} {noWebGL && ( 3D Preview Buka di browser dengan WebGL Animasi 3D box → ingredients akan muncul di sini. )} {/* Ingredient cards around the sachet — clickable */} {INGREDIENTS.map((ing, i) => { const visible = progress >= ing.threshold; const showHint = ing.key === 'kayu-rapat'; return ( setSelected(ing)} aria-label={'Detail ' + ing.name} > {showHint && ( )} {String(i + 1).padStart(2, '0')} {ing.name} Lihat manfaat ); })} {/* Ingredient detail popup */} {selected && ( setSelected(null)}> e.stopPropagation()}> setSelected(null)} aria-label="Tutup"> Bahan Alami Yojea {selected.name} {selected.latin} {selected.desc} {selected.tags.map(t => {t})} )} {/* Progress bar at bottom */} Klik komposisi untuk melihat detail Scroll untuk eksplor ); } Object.assign(window, { Section2 });
{selected.desc}