Single Progress Uniform: GSAP's Bridge to WebGL Shaders

Control complex shader effects with one JS-tweened progress value (0 to 1) copied to uniforms each frame. Shaders stay stateless—use step(noiseVal, progress) for block reveals, parabola(progress, 2.) for warp timing (dt), and dt * uRGBShift for chromatic aberration peaks. In video carousel transitions, mix source/dest textures: gl_FragColor = mix(t1, t2, intpl) where intpl = step(noiseVal, progress). Swap eases or chain timelines in GSAP without GLSL edits. Reuse across effects: flowmap text distortion samples velocity RG texture, applies directional offsets (normalize(toMouse) * influence), and velocity-triggered rainbows via sin(hueShift + 2π/3 rad) phases for R/G/B only when uVelo > 0.01. Outcome: tight motion curves, no per-effect JS logic.

Mount one FlowmapEffect at page level, swap imageSrc textures—avoids GPU context respawns, prevents memory leaks/stutters after multiple hovers. Idle guard pauses rAF after 90 frames sans mousemove, resumes instantly.

Scroll-Driven Morphs and Direct DOM Writes

Pin next-project previews with ScrollTrigger(scrub: 1); onUpdate writes progress-derived values directly to DOM: textContent for counters (Math.round(progress * 100)), style.transform/clipPath for background morphs (scale(1.3 - 0.3 * progress), inset(${insetV}% ${insetH}% ...)), strokeDashoffset for SVG circles (CIRCUMFERENCE - progress * CIRCUMFERENCE). Bypasses React setState()—saves render trees per frame on 120Hz displays, ensures 60fps butter over slideshows.

Three-state machine (idle → triggered → navigating) prevents phantom navs: require hasSeenLowProgress flag (scrolled from top), velocity < 2000 (scrollTrigger.getVelocity()), 250ms commit timeout. Scroll back triggers onLeaveBack rollback. Clicks spawn GSAP tween from current progress to 1, syncing scroll—same DOM mutations, dual drivers.

Page exits stage GSAP fades (bg/grid/texts at 0.3s power2.inOut, content at 0.35s offset 0.25s) parallel to import() chunks and image preloads. flushSync(navigate(path)) inside document.startViewTransition forces sync React commits—avoids double old-DOM captures.

Unified Text Reveals and Perf Habits

Central scrambleText utility pairs SplitText (chars/lines) with parallel clip-path wipe (inset(0 0% 0 0) over 0.6s power2.out): left-resolving scramble + left-to-right wipe hides noise, reveals only legible text. Pre-scramble to exact length, lock height via getBoundingClientRect().height. SCRAMBLE_CHARS = 'A!B@C#D$E%F&G*H?J[K]L{M}N=O+P-QRSTUVWXYZ' mixes punctuation for 'decoding' feel.

Perf across frames: element.style.*/textContent over state; boundRender = this.render.bind(this); Vec2.set() over new Vec2(); needsUpdate = true only on active video textures, fallback to <video>; precompute image-dimensions.json/lqip-data.json. prefers-reduced-motion: reduce as parallel design (per Cassie Evans), not disable.

Stack: Vite/React18/TS (fast loop), OGL (lighter than Three.js), Lenis (smooth scroll sync), SCSS/BEM. Hooks for lifecycle, services (e.g., Lenis singleton) for globals. Figma grid exported to CSS, toggle Cmd/Ctrl + G. Tradeoff: OGL's lean API owned line-by-line vs Three.js detour.