/* ═══════════════════════════════════════════════════════════════ DNAHelix - Premium Raymarched DNA Shader ═══════════════════════════════════════════════════════════════ */ const VERTEX = ` attribute vec2 position; attribute vec2 uv; varying vec2 vUv; void main() { vUv = uv; gl_Position = vec4(position, 0.0, 1.0); } `; const FRAGMENT = ` precision highp float; uniform float uTime; uniform float uScroll; uniform vec2 uResolution; uniform vec2 uMouse; uniform vec3 uColor1; uniform vec3 uColor2; uniform float uOpacity; uniform float uGlow; uniform float uScale; varying vec2 vUv; #define MAX_STEPS 70 #define MAX_DIST 15.0 #define SURF_DIST 0.005 // 2D Rotation mat2 rot(float a) { float s = sin(a); float c = cos(a); return mat2(c, -s, s, c); } // Capsule SDF float sdCapsule(vec3 p, vec3 a, vec3 b, float r) { vec3 pa = p - a, ba = b - a; float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); return length(pa - ba * h) - r; } // Smooth min float smin(float a, float b, float k) { float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0); return mix(b, a, h) - k * h * (1.0 - h); } // Scene Distance Field float map(vec3 p) { // Basic scaling and scrolling p.y += uScroll * 0.003; p /= uScale; // Global subtle rotation (mouse + auto) p.yz *= rot(uMouse.y * 0.5); p.xz *= rot(uMouse.x * 0.5 + uTime * 0.15); p.xy *= rot(0.1 * sin(uTime * 0.3)); // Subtle breathing tilt vec3 q = p; // Twist deformation float twistAmt = 1.2; q.xz *= rot(q.y * twistAmt - uTime * 0.8); // Double Helix dimensions float radius = 0.65; float thickness = 0.06; // Two vertical strands float s1 = length(q.xz - vec2(radius, 0.0)) - thickness; float s2 = length(q.xz - vec2(-radius, 0.0)) - thickness; // Base pairs (Rungs) float spacing = 0.5; vec3 rp = q; rp.y = mod(rp.y + spacing * 0.5, spacing) - spacing * 0.5; float rung = sdCapsule(rp, vec3(radius, 0.0, 0.0), vec3(-radius, 0.0, 0.0), 0.015); // Organic blending float dna = smin(smin(s1, s2, 0.2), rung, 0.15); // Subtle organic pulsing dna += sin(p.y * 12.0 - uTime * 3.0) * 0.005; return dna * uScale; } // Normal Calculation vec3 getNormal(vec3 p) { vec2 e = vec2(0.005, 0.0); return normalize(vec3( map(p + e.xyy) - map(p - e.xyy), map(p + e.yxy) - map(p - e.yxy), map(p + e.yyx) - map(p - e.yyx) )); } void main() { // Screen coordinates centered vec2 uv = (vUv - 0.5) * 2.0; uv.x *= uResolution.x / uResolution.y; // Ray setup vec3 ro = vec3(0.0, 0.0, 3.5); vec3 rd = normalize(vec3(uv, -1.0)); float d0 = 0.0; vec3 p; float glow = 0.0; // Raymarching loop for(int i = 0; i < MAX_STEPS; i++) { p = ro + rd * d0; float dS = map(p); // Accumulate volumetric glow when close to surface if (dS < 0.15) { glow += (0.15 - dS) * 0.8; } if(dS < SURF_DIST || d0 > MAX_DIST) break; d0 += dS * 0.7; // Fractional step for better glow sampling } vec3 col = vec3(0.0); float alpha = 0.0; if(d0 < MAX_DIST) { // Surface shading vec3 n = getNormal(p); vec3 lig = normalize(vec3(1.0, 2.0, 3.0)); vec3 lig2 = normalize(vec3(-2.0, -1.0, -2.0)); // Basic lighting float dif = max(dot(n, lig), 0.0); float dif2 = max(dot(n, lig2), 0.0) * 0.5; float amb = 0.15 + p.z * 0.02; // Depth ambient // Determine coloring based on untwisted space vec3 untwisted = p / uScale; untwisted.xz *= rot(-untwisted.y * 1.2 + uTime * 0.8); float colorBlend = smoothstep(-0.3, 0.3, untwisted.x); vec3 matCol = mix(uColor1, uColor2, colorBlend); // Fresnel rim light float fresnel = pow(1.0 - max(dot(n, -rd), 0.0), 3.0); col = matCol * (dif + dif2 + amb); col += matCol * fresnel * 0.8; // Rim light enhancement // Atmospheric depth fading float fog = smoothstep(2.5, 6.0, d0); col = mix(col, vec3(0.0), fog); alpha = 1.0 - fog; } // Add volumetric glow vec3 glowCol = mix(uColor1, uColor2, 0.5); float glowIntensity = glow * uGlow * 0.02; col += glowCol * glowIntensity; alpha += glowIntensity; // Final output with global opacity gl_FragColor = vec4(col, min(alpha * uOpacity, 1.0)); } `; const DNAHelix = ({ primaryColor = '#CBF696', secondaryColor = '#64B400', speed = 0.6, scale = 1.0, opacity = 0.95, glow = 0.8, mouseInteractive = true, className, style, }) => { const containerRef = React.useRef(null); const oglRef = React.useRef(null); const mouseRef = React.useRef([0, 0]); const targetMouseRef = React.useRef([0, 0]); const hexToRgb = (hex) => { let c = hex.replace(/^#/, ''); if (c.length === 3) c = c.split('').map(x => x + x).join(''); const num = parseInt(c, 16); return [((num >> 16) & 255) / 255, ((num >> 8) & 255) / 255, (num & 255) / 255]; }; React.useEffect(() => { const ogl = window.ogl; if (!ogl) { console.warn('DNAHelix: OGL not found on window.'); return; } const { Renderer, Program, Mesh, Triangle, Color } = ogl; const el = containerRef.current; if (!el) return; if (oglRef.current) { const p = oglRef.current; p.ro.disconnect(); cancelAnimationFrame(p.raf); if (p.renderer.gl.canvas.parentElement === el) { el.removeChild(p.renderer.gl.canvas); } oglRef.current = null; } // This is a 70-step raymarch running over every pixel of the canvas, so the // render target's area is the whole cost of the shader. At dpr 2 on a retina // display that is ~14 megapixels a frame, which starves the compositor that // is driving the scroll. The SDFs are already smooth-min'd and the whole // thing sits at 0.5 opacity behind text, so neither the extra device pixels // nor MSAA are visible — dropping both is most of the frame budget back. const renderer = new Renderer({ alpha: true, antialias: false, dpr: Math.min(window.devicePixelRatio || 1, 1.25) }); const gl = renderer.gl; el.appendChild(gl.canvas); gl.canvas.style.width = '100%'; gl.canvas.style.height = '100%'; gl.canvas.style.display = 'block'; const geometry = new Triangle(gl); const program = new Program(gl, { vertex: VERTEX, fragment: FRAGMENT, uniforms: { uTime: { value: 0 }, uScroll: { value: 0 }, uResolution: { value: [1, 1] }, uMouse: { value: [0, 0] }, uColor1: { value: new Color(...hexToRgb(primaryColor)) }, uColor2: { value: new Color(...hexToRgb(secondaryColor)) }, uOpacity: { value: opacity }, uGlow: { value: glow }, uScale: { value: scale }, }, transparent: true, depthTest: false, }); const mesh = new Mesh(gl, { geometry, program }); const resize = () => { const w = el.clientWidth || 1; const h = el.clientHeight || 1; renderer.setSize(w, h); program.uniforms.uResolution.value = [w, h]; }; resize(); const ro = new ResizeObserver(resize); ro.observe(el); const onMouseMove = (e) => { if (!mouseInteractive) return; // Normalize mouse to -1 to 1 targetMouseRef.current[0] = (e.clientX / window.innerWidth) * 2 - 1; targetMouseRef.current[1] = -(e.clientY / window.innerHeight) * 2 + 1; }; window.addEventListener('mousemove', onMouseMove, { passive: true }); let raf = 0; let running = false; // Wall-clock time is paused along with the loop, so the helix picks up from // where it was rather than jumping forward by however long it was parked. let clock = 0; let lastT = 0; const loop = (t) => { raf = requestAnimationFrame(loop); clock += Math.min(t - lastT, 50); // a long stall must not snap the animation lastT = t; program.uniforms.uTime.value = clock * 0.001 * speed; // Read the scroll here rather than from a scroll listener: it is only ever // consumed by a frame we are actually drawing, and window.scrollY inside // rAF costs nothing (layout is already clean at that point). program.uniforms.uScroll.value = window.scrollY; // Smoothly interpolate mouse mouseRef.current[0] += (targetMouseRef.current[0] - mouseRef.current[0]) * 0.05; mouseRef.current[1] += (targetMouseRef.current[1] - mouseRef.current[1]) * 0.05; program.uniforms.uMouse.value = mouseRef.current; renderer.render({ scene: mesh }); }; const start = () => { if (running) return; running = true; lastT = performance.now(); raf = requestAnimationFrame(loop); }; const stop = () => { if (!running) return; running = false; cancelAnimationFrame(raf); raf = 0; }; // The hero is the first screen of a long page, so for most of a visit this // shader is off-screen. Left running it keeps the GPU saturated and the // scroll compositing behind it — which is why the page felt heavy well past // the hero. Only draw while some of the canvas is actually on screen. // // Both triggers re-derive the answer rather than sharing a cached flag. A // page opened in a background tab gets no IntersectionObserver callbacks at // all — observer delivery rides the frame loop, which is frozen — so a flag // set only by the observer is still false when the tab is finally revealed, // and the shader would never start. Measuring costs one rect on a tab // switch or a viewport crossing, which is nothing. const MARGIN = 100; const shouldRun = () => { if (document.hidden) return false; const r = el.getBoundingClientRect(); return r.bottom > -MARGIN && r.top < window.innerHeight + MARGIN; }; const sync = () => (shouldRun() ? start() : stop()); const io = new IntersectionObserver(sync, { rootMargin: `${MARGIN}px` }); io.observe(el); document.addEventListener('visibilitychange', sync); // The observer's first delivery is a frame away, and on a tab that loads // hidden it never arrives; the hero is on screen at load, so just start. sync(); oglRef.current = { renderer, ro, raf }; return () => { ro.disconnect(); io.disconnect(); stop(); document.removeEventListener('visibilitychange', sync); window.removeEventListener('mousemove', onMouseMove); if (gl.canvas.parentElement === el) { el.removeChild(gl.canvas); } oglRef.current = null; }; }, [primaryColor, secondaryColor, speed, scale, opacity, glow, mouseInteractive]); return (
); }; window.DNAHelix = DNAHelix;