// Arise Helix — DNA strand that morphs with scroll. // 6 visual states keyed to sections: hero, about, programs, lab, testimonials, cta. function lerp(a, b, t) { return a + (b - a) * t; } function smoothStep(t) { return t * t * (3 - 2 * t); } /* Build a single helix strand path between y=0 and y=h with given amplitude, frequency, phase, baseline x. */ function helixPath({ h = 1000, amp = 80, freq = 4, phase = 0, baseX = 0, samples = 120 }) { let d = ""; for (let i = 0; i <= samples; i++) { const t = i / samples; const y = t * h; const x = baseX + Math.sin(t * freq * Math.PI * 2 + phase) * amp; d += (i === 0 ? "M " : "L ") + x.toFixed(2) + " " + y.toFixed(2) + " "; } return d.trim(); } /* Rungs between the two strands. */ function helixRungs({ h = 1000, amp = 80, freq = 4, phase = 0, baseX = 0, count = 28 }) { const lines = []; for (let i = 0; i < count; i++) { const t = (i + 0.5) / count; const y = t * h; const x1 = baseX + Math.sin(t * freq * Math.PI * 2 + phase) * amp; const x2 = baseX + Math.sin(t * freq * Math.PI * 2 + phase + Math.PI) * amp; lines.push({ x1, y1: y, x2, y2: y, t }); } return lines; } // Per-section helix parameters const STATES = [ // 0 hero — tight elegant { amp: 70, freq: 5.0, opacity: 0.55, rotate: 6, scale: 1.0, rungCount: 26, color: 0 }, // 1 about — wider, slower { amp: 130, freq: 2.6, opacity: 0.45, rotate: -4, scale: 1.05, rungCount: 22, color: 0.1 }, // 2 programs — energetic { amp: 110, freq: 7.5, opacity: 0.55, rotate: 10, scale: 1.0, rungCount: 36, color: 0.25 }, // 3 lab — featured, larger { amp: 150, freq: 3.5, opacity: 0.65, rotate: -2, scale: 1.2, rungCount: 24, color: 0.4 }, // 4 testimonials — soft wave, lower freq { amp: 95, freq: 1.8, opacity: 0.35, rotate: 4, scale: 1.0, rungCount: 18, color: 0.5 }, // 5 cta — converges into single line { amp: 8, freq: 1.0, opacity: 0.85, rotate: 0, scale: 1.0, rungCount: 0, color: 0.8 }, ]; function HelixStrand() { const progress = usePageProgress(); const activeIdx = useActiveSection([ "helix-hero", "helix-about", "helix-programs", "helix-lab", "helix-testimonials", "helix-cta", ]); const [s, setS] = React.useState(STATES[0]); React.useEffect(() => { let raf; const target = STATES[activeIdx] || STATES[0]; const tick = () => { setS((cur) => { const next = {}; let stillMoving = false; Object.keys(target).forEach((k) => { const v = lerp(cur[k], target[k], 0.08); if (Math.abs(v - target[k]) > 0.001) stillMoving = true; next[k] = v; }); if (stillMoving) raf = requestAnimationFrame(tick); return next; }); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [activeIdx]); const [phase, setPhase] = React.useState(0); React.useEffect(() => { let raf; const start = performance.now(); const tick = (now) => { const t = (now - start) / 1000; setPhase(t * 0.45 + progress * 6); raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [progress]); const W = 400; const H = 1100; const baseX = W / 2; // Render dense set of nucleotide pairs along the helix, depth-sorted by z so // the result reads like a 3D rendered DNA rather than a flat SVG. const N = 64; // rungs const ampMax = s.amp; const opacity = s.opacity; const pairs = []; for (let i = 0; i < N; i++) { const t = (i + 0.5) / N; const y = t * H; const ang = t * s.freq * Math.PI * 2 + phase; const sinA = Math.sin(ang); const cosA = Math.cos(ang); // z-depth proxy (-1..1) const xa = baseX + sinA * ampMax; const xb = baseX - sinA * ampMax; pairs.push({ i, t, y, xa, xb, za: cosA, zb: -cosA }); } // Build strand ribbon points (left/right) sampled densely so the curves are smooth. const RIBBON_SAMPLES = 240; const strandA = []; const strandB = []; for (let i = 0; i <= RIBBON_SAMPLES; i++) { const t = i / RIBBON_SAMPLES; const y = t * H; const ang = t * s.freq * Math.PI * 2 + phase; const sinA = Math.sin(ang); const cosA = Math.cos(ang); const xa = baseX + sinA * ampMax; const xb = baseX - sinA * ampMax; strandA.push({ x: xa, y, z: cosA }); strandB.push({ x: xb, y, z: -cosA }); } const toPath = (pts) => pts.map((p, i) => (i === 0 ? "M " : "L ") + p.x.toFixed(2) + " " + p.y.toFixed(2)).join(" "); // Sphere visual params — derived from z so the helix has parallax depth. const nucleoRadius = (z) => { // z in [-1,1] — front bigger, back smaller const norm = (z + 1) / 2; // 0..1 return 4.5 + norm * 5.0; }; const nucleoOpacity = (z) => { const norm = (z + 1) / 2; return 0.55 + norm * 0.45; }; // Sort rungs by z so back rungs render first, front rungs on top. const sortedPairs = [...pairs].sort((a, b) => a.za - b.za); return (
{/* Strand ribbon gradient (along length) */} {/* Ribbon highlight (thin, for shine) */} {/* Sphere radial — front-lit nucleotide */} {/* Sphere radial — back-lit nucleotide (cooler, deeper) */} {/* Rung shading */} {/* Outer glow halo behind both strands */} {/* RENDER ORDER: back rungs → back strand segments → back nucleotides → front rungs → front strand → front nucleotides */} {/* Back rungs (z < 0) */} {sortedPairs.filter((p) => p.za < 0).map((p) => { const front = p.za >= 0; const dx = p.xb - p.xa; const dy = p.yb !== undefined ? 0 : 0; // rungs are horizontal here return ( ); })} {/* Back-half strands rendered with low opacity */} {/* Back nucleotides */} {sortedPairs.filter((p) => p.za < 0).map((p) => ( = 0 ? "url(#sphereFront)" : "url(#sphereBack)"} opacity={nucleoOpacity(p.zb) * opacity} /> ))} {/* Front rungs (z >= 0) */} {sortedPairs.filter((p) => p.za >= 0).map((p) => ( {/* Inner shine on rung */} ))} {/* Front-half strand ribbons — thicker with shine highlight */} {/* Front nucleotides — render large 3D spheres */} {sortedPairs.filter((p) => p.za >= 0).map((p) => ( {/* outer rim (glow) */} {/* tiny highlight */} = 0 ? "url(#sphereFront)" : "url(#sphereBack)"} opacity={nucleoOpacity(p.zb) * opacity} /> {p.zb >= 0 && ( )} ))}
); } /* ──────────────── Cursor radial glow ──────────────── */ function CursorGlow() { const { x, y } = useCursor(); if (x < 0) return null; return (
); } /* ──────────────── Floating particles ──────────────── */ function HelixParticles({ count = 18 }) { const items = React.useMemo( () => Array.from({ length: count }).map((_, i) => ({ left: (i * 17.3) % 100, size: 2 + (i % 4) * 1.2, delay: (i * 0.83) % 16, duration: 18 + (i % 7) * 2.4, gold: i % 3 === 0, })), [count] ); return (
{items.map((p, i) => ( ))}
); } /* ──────────────── Section label pill ──────────────── */ function SectionLabel({ children }) { return (
{children}
); } Object.assign(window, { HelixStrand, CursorGlow, HelixParticles, SectionLabel, ScrollVideoBg }); /* ──────────────── Scroll-scrubbed video background ──────────────── */ // IndexedDB helpers for frame cache — survives reloads so only the first visit // shows the "Preparing helix" screen. Bump CACHE_VERSION to invalidate. const HELIX_DB_NAME = "helix-dna-cache"; const HELIX_DB_STORE = "frames"; const HELIX_CACHE_VERSION = 1; function helixOpenDB() { return new Promise((resolve, reject) => { const req = indexedDB.open(HELIX_DB_NAME, 1); req.onupgradeneeded = () => { const db = req.result; if (!db.objectStoreNames.contains(HELIX_DB_STORE)) db.createObjectStore(HELIX_DB_STORE); }; req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } async function helixCacheGet(key) { const db = await helixOpenDB(); return new Promise((resolve, reject) => { const tx = db.transaction(HELIX_DB_STORE, "readonly"); const req = tx.objectStore(HELIX_DB_STORE).get(key); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } async function helixCachePut(key, value) { const db = await helixOpenDB(); return new Promise((resolve, reject) => { const tx = db.transaction(HELIX_DB_STORE, "readwrite"); tx.objectStore(HELIX_DB_STORE).put(value, key); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } function ScrollVideoBg({ src }) { const canvasRef = React.useRef(null); const stateRef = React.useRef({ frames: [], fps: 24, w: 0, h: 0, ready: false }); const [progress, setProgress] = React.useState(0); // 0..1 extraction progress const [ready, setReady] = React.useState(false); // Detect mobile/touch devices — they get a simple