Monolith — WebGL Portfolio
A cinematic single-page portfolio built on a Three.js hero object with custom shader displacement, a scroll-driven camera dolly, and an index-style project list. Four sections, heavy on motion.

The prompt — 4 sections
Scene Foundation
Build a single-page portfolio site called **Monolith** for a motion designer. The centerpiece is a full-viewport WebGL scene that persists across the whole page while HTML content scrolls over it.
- **Stack:** React + Tailwind + Three.js. One canvas mounted at the root, `position: fixed`, `inset: 0`, `z-index: 0`. All HTML sits at `z-index: 10` with transparent backgrounds. Drive `three` directly rather than through react-three-fiber: the scene is one persistent object and one render loop, so a reconciler buys nothing here and costs you two more dependencies. Import `three` inside an effect, never at module scope, so a visitor who never reaches the scene does not pay for it.
- **Palette:** page `#080808`, text `#f0ede8`, muted `#736d66`. One accent: `#ff4d1c`. The scene supplies all other color through light, not materials.
- **Renderer:** ACES filmic tone mapping, exposure 1.1, antialias on, alpha true, and pixel ratio clamped to `Math.min(devicePixelRatio, 2)` — uncapped DPR on a 3× phone renders nine times the pixels for no visible gain. Grade in the material's own fragment shader rather than adding a composer: a bloom-and-grain pass is two extra full-screen draws, and at this scene's contrast you can fake both in one.
- **Lighting:** one key directional light from the upper left at intensity 2.5, one accent-colored rim light from behind at intensity 4, and an environment map set to "city" at 0.4 intensity.
- **Performance:** pause the render loop when the canvas is off-screen or the tab is hidden. If WebGL is unavailable or `prefers-reduced-motion` is set, render a static poster image instead and skip the canvas entirely.
Hero Object
The hero is a single rotating form at the center of the scene.
Geometry: an icosahedron with detail level 64, radius 1.6. Write a custom `ShaderMaterial` for it:
- The vertex shader displaces each vertex along its normal by 3D simplex noise sampled at `position * 1.4 + time * 0.15`, scaled by an `uAmplitude` uniform that starts at 0.28.
- The fragment shader shades by fresnel: `pow(1.0 - dot(viewDirection, normal), 2.5)`, mixing from near-black at facing angles to the accent color at grazing angles. Add a thin bright rim by boosting anything above 0.9 fresnel.
- Uniforms: `uTime`, `uAmplitude`, `uMouse`, `uAccent`.
Motion: rotate on Y at 0.08 rad/s. Lerp `uMouse` toward the normalized pointer position at 0.05 per frame and offset the noise lookup by it, so the surface subtly follows the cursor with lag. On pointer down, ease `uAmplitude` to 0.55 over 300ms and back on release.
Over the canvas, place the HTML hero: a mono eyebrow reading the current year and location, a headline at clamp(3rem, 9vw, 8rem) in a high-contrast display serif reading "Monolith", set in the page text color with `mix-blend-mode: difference` so it inverts against the object behind it, and a single line of muted body copy. Bottom-left, a scroll hint that fades out after the first 200px of scroll.
Scroll Camera & Project Index
Bind the camera to scroll progress so the whole page reads as one continuous shot.
Camera path: start at `[0, 0, 5]` looking at origin. As scroll progresses 0 → 1, dolly to `[2.2, -0.6, 2.4]` and ease the look-at target to `[0, -0.4, 0]`. Interpolate with a damped lerp at 0.06 per frame rather than binding directly to scroll, so the camera trails the scroll with weight. Simultaneously ease `uAmplitude` from 0.28 down to 0.08, so the form calms as the visitor descends.
Over this, render the project index as an editorial list, not a card grid. Each row is a full-width flex line: a two-digit mono number, the project title in the display serif at 40px, a muted mono year on the right, and a 1px hairline rule beneath. Twelve rows.
Row interaction: on hover, the title slides 16px right over 400ms, the number goes accent-colored, the rule brightens, and a 240×160 preview image appears pinned to the cursor with a 60ms trailing lerp and a slight rotation proportional to pointer velocity. Only one preview is ever visible. On touch devices, drop the cursor preview and show a thumbnail inline at the left of each row.
Closing & Contact
Close the page by bringing the WebGL object back to prominence.
In the final section, ease the camera back toward the object and raise `uAmplitude` to 0.7, so the form churns as the page ends. Fade the page background to pure black over the last 40% of scroll.
Content over it: a headline reading "Available for select work" at clamp(2rem, 5vw, 4rem), an email address at 32px that copies itself to the clipboard on click and swaps to "Copied" for 1.5 seconds, and a row of four text links to social profiles, each with an underline that wipes in from the left on hover over 300ms.
End with a minimal footer bar: local time updating live in mono on the left, and a "Back to top" link on the right that smooth-scrolls over 900ms with an ease-in-out curve.


