Overview
A cinematic single-page portfolio built for a visual effects context. The goal: make the browser feel like a 3D environment, not a webpage. Every scroll event is a deliberate camera move. Every hover is a material response.
Built with React Three Fiber, GSAP ScrollTrigger, Lenis, and hand-written GLSL shaders.
What was built
Particle field — 14,000 GPU particles driven by a custom vertex shader. Curl-noise flow field, mouse repel on hover, scroll-triggered dispersion (particles scatter as you leave the hero). Additive blending + depthWrite: false keeps overdraw cheap. Runs at 60fps on most GPUs.
Kinetic headline — GSAP ScrollTrigger linked headline: letter-spacing expands, skew rotates, words stagger in/out as you scroll. No layout triggers — transform and opacity only.
Stacking project cards — GSAP ScrollTrigger pin + overlap. Each project card slides under the next, creating a physical stacking feel. The scroll scrub is synced to Lenis for perfect smooth physics.
Project planes — Per-project R3F planes with a custom wave + RGB-split shader. Wave amplitude responds to mouse proximity. RGB channels separate on hover for a chromatic aberration effect.
Lenis + GSAP sync — Lenis drives the GSAP ticker directly via gsap.ticker.add. Every scroll effect is synced to the same RAF — no jitter between the particle field and the GSAP animations.
Stack
| Layer | Technology | |-------|-----------| | Build | Vite + React | | 3D | React Three Fiber + Drei | | Shaders | GLSL (custom vertex + fragment) | | Animation | GSAP + ScrollTrigger | | Scroll | Lenis | | Styling | Tailwind CSS | | Deploy | Vercel (Vite preset) |
Performance notes
- Particle count: 14,000 — safe on most GPUs at 60fps. Drop to 8k for mobile.
dpr={[1, 2]}caps DPR at 2× for retina without thrashing lower-end GPUs.- All scroll effects are transform + opacity only. Zero layout triggers.
- The grain overlay is a
fixed inset-0canvas — never attached to a scroll container.
Key decisions
Why R3F over Three.js direct? React component model makes scene management dramatically simpler. useFrame, useThree, and Drei utilities cut boilerplate to near-zero while keeping full Three.js escape hatches.
Why Lenis over native scroll? Consistent scroll physics across browsers. The ease curve is controllable — the portfolio needed a specific "heavy" feel, not the snappy default browser scroll.
Why Vite not Next.js? No SSR needed for a purely visual portfolio. Vite's HMR is faster for shader iteration — a critical feedback loop when tuning GLSL.





