// 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 (
);
}
/* ──────────────── 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