# Fractal Glass Parallax Hero — build prompt

## Goal
Build a full-viewport hero for a fictional design studio called **"Glassform"**. A single editorial portrait fills the screen, but it is not shown directly: it is rendered on a WebGL plane and refracted through a custom fragment shader that bends the image into dozens of vertical **fractal-glass ribbons** (like looking through fluted / reeded glass). Moving the mouse feeds a smoothed pointer position into the shader that produces a subtle **horizontal parallax**, amplified inside the distorted stripes so the ribbons appear to slide over each other. The star effect is the shader itself, driven by a lerped mouse uniform on a `requestAnimationFrame` loop.

## Tech
Vanilla HTML/CSS/JS with ES module imports. Use **`three`** (npm) only — there is **no GSAP and no scroll library** in this component. All motion comes from the Three.js render loop + a GLSL fragment shader. Assume a fresh Vite project; `three` is installed via npm and imported as `import * as THREE from "three"`.

Split the code into three files:
- `index.html`
- `styles.css`
- `script.js` — the Three.js app (`<script type="module" src="./script.js">`)
- Put the GLSL strings in a small `shaders.js` module that exports `vertexShader` and `fragmentShader`, imported by `script.js`. (Inlining them in `script.js` is also fine.)

## Layout / HTML
```
nav
  .logo > a            → "Ω Glassform"   (use the Ω ohm sign, HTML entity &#8486;, before the word)
  .nav-links > a ×3    → "Experiments", "Objects", "Exhibits"
section.hero
  img#glassTexture     → src to the hero image, alt=""   (this <img> is hidden via CSS; it is only a source for the WebGL texture)
  .hero-content
    h1                 → "Designed for the space between silence and noise."
    p                  → "Developed by Glassform"
```
The Three.js renderer's `<canvas>` is appended to `.hero` by JS at runtime (it becomes a child of `.hero`).

## Styling
- Font: **Manrope** (Google Fonts, variable weight `200..800`). `body { font-family: "Manrope", sans-serif; }`.
- Colors: pure black background `#000`, pure white text `#fff`. No other colors in CSS — all color comes from the refracted image.
- Global reset: `* { margin:0; padding:0; box-sizing:border-box; }`.
- `h1`: `font-size: 4rem; font-weight: 500; letter-spacing: -0.1rem; line-height: 1;`
- `a, p`: `color:#fff; text-decoration:none; font-size:0.85rem; font-weight:550; line-height:1; display:inline-block;`
- `nav`: `position:fixed; width:100%; padding:2rem; display:flex; justify-content:space-between; align-items:flex-start; z-index:2;`
- `nav .nav-links`: `display:flex; gap:0.75rem;`
- `.hero`: `position:relative; width:100%; height:100svh; overflow:hidden;`
- `.hero img#glassTexture`: `display:none;` (critical — the raw `<img>` must never be visible; it only feeds the texture).
- `.hero-content`: `position:absolute; left:0; bottom:0; width:100%; padding:2rem; display:flex; justify-content:space-between; align-items:flex-end;` (it paints above the static WebGL canvas because it is positioned).
- `.hero-content h1`: `width:60%;`
- Responsive `@media (max-width:1000px)`: `.hero-content { align-items:flex-start; flex-direction:column-reverse; gap:1rem; }` and `.hero-content h1 { width:100%; }`.

## The effect (be exact — this is the whole component)

### Three.js scene setup
- `scene = new THREE.Scene()`.
- `camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1)` — left/right/top/bottom/near/far. This maps a `[-1,1]` quad to the full viewport.
- `renderer = new THREE.WebGLRenderer({ antialias: true })`; `renderer.setSize(window.innerWidth, window.innerHeight)`; `renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))`; append `renderer.domElement` to the `.hero` container.
- Geometry: `new THREE.PlaneGeometry(2, 2)` (fills the whole ortho space). `mesh = new THREE.Mesh(geometry, material)`; `scene.add(mesh)`.
- Material: `THREE.ShaderMaterial` with the uniforms below plus the vertex + fragment shaders.

### Config constants (exact values — they define the look)
```
lerpFactor:            0.035   // pointer smoothing per frame
parallaxStrength:      0.1     // base parallax amount (uParallaxStrength)
distortionMultiplier:  10      // how much the ribbons amplify parallax (uDistortionMultiplier)
glassStrength:         2.0     // displacement gain per stripe (uGlassStrength)
glassSmoothness:       0.0001  // sample spacing for the 11-tap blur (uglassSmoothness)
stripesFrequency:      35      // number of vertical glass ribbons (ustripesFrequency)
edgePadding:           0.1     // fade width at left/right edges (uEdgePadding)
```

### Uniforms (initial values)
- `uTexture`: `null` initially, set once the image loads.
- `uResolution`: `vec2(window.innerWidth, window.innerHeight)`.
- `uTextureSize`: `vec2(1, 1)` initially, set to the image's natural pixel size on load.
- `uMouse`: `vec2(0.5, 0.5)` (centered).
- `uParallaxStrength` = 0.1, `uDistortionMultiplier` = 10, `uGlassStrength` = 2.0, `ustripesFrequency` = 35, `uglassSmoothness` = 0.0001, `uEdgePadding` = 0.1.

### Texture loading
The hidden `#glassTexture` `<img>` is the source. When it is `complete` (or on its `onload`), build `new THREE.Texture(imgElement)`, read `naturalWidth`/`naturalHeight` into `uTextureSize`, set `texture.needsUpdate = true`, and assign it to `uTexture`. Handle both the already-cached case and the async `onload` case.

### Pointer smoothing (the only "animation" driver)
- Keep two objects: `mouse = {x:0.5, y:0.5}` (current, smoothed) and `targetMouse = {x:0.5, y:0.5}` (raw target).
- `window` `mousemove`: `targetMouse.x = e.clientX / innerWidth`; `targetMouse.y = 1.0 - e.clientY / innerHeight` (**note the Y flip** — WebGL origin is bottom-left).
- `lerp(a, b, f) = a + (b - a) * f`.
- In the render loop every frame: `mouse.x = lerp(mouse.x, targetMouse.x, 0.035)`, same for `y`, then `uMouse.value.set(mouse.x, mouse.y)`. This gives a slow, floaty follow (~0.035 easing factor).

### Render loop
`function animate(){ requestAnimationFrame(animate); /* lerp mouse + update uMouse */ renderer.render(scene, camera); }` — call `animate()` once. Runs continuously; no start/stop trigger.

### Resize
On `window` `resize`: `renderer.setSize(innerWidth, innerHeight)` and update `uResolution` to the new size. (Do not touch `uTextureSize`.)

### Vertex shader
Pass-through: a `varying vec2 vUv`; `vUv = uv;` `gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);`.

### Fragment shader (the fractal-glass refraction — reproduce this math exactly)
Helpers:
- **`getCoverUV(uv, textureSize)`** — emulates CSS `background-size: cover`. If either `textureSize` component `< 1.0`, return `uv` unchanged. Else: `s = uResolution / textureSize; scale = max(s.x, s.y); scaledSize = textureSize * scale; offset = (uResolution - scaledSize) * 0.5; return (uv * uResolution - offset) / scaledSize;`
- **`displacement(x, num_stripes, strength)`** → `modulus = 1.0 / num_stripes; return mod(x, modulus) * strength;` (a per-stripe sawtooth ramp).
- **`fractalGlass(x)`** — an 11-tap box blur of the sawtooth: `float d = 0.0; for (int i = -5; i <= 5; i++) { d += displacement(x + float(i) * uglassSmoothness, ustripesFrequency, uGlassStrength); } d /= 11.0; return x + d;` (i.e. offset X by the averaged displacement — this is what "bends" the image into ribbons).
- **`smoothEdge(x, padding)`** — returns 1.0 in the middle and fades the effect to 0 at the left/right edges: `edge = padding; if (x < edge) return smoothstep(0.0, edge, x); else if (x > 1.0 - edge) return smoothstep(1.0, 1.0 - edge, x); return 1.0;` (note the descending `smoothstep(1.0, 1.0-edge, x)` on the right side).

`main()` order:
1. `vec2 uv = vUv; float originalX = uv.x;`
2. `float edgeFactor = smoothEdge(originalX, uEdgePadding);`
3. `float distortedX = fractalGlass(originalX);`
4. `uv.x = mix(originalX, distortedX, edgeFactor);` — apply the ribbon distortion, faded near edges.
5. `float distortionFactor = uv.x - originalX;` — how much this pixel got bent.
6. Parallax:
   - `float parallaxDirection = -sign(0.5 - uMouse.x);` (mouse right of center ⇒ +1, left ⇒ −1).
   - `vec2 parallaxOffset = vec2( parallaxDirection * abs(uMouse.x - 0.5) * uParallaxStrength * (1.0 + abs(distortionFactor) * uDistortionMultiplier), 0.0 );` — horizontal only, scaled by pointer distance from center and **amplified where the glass is most distorted**.
   - `parallaxOffset *= edgeFactor;` (also fade parallax at edges).
   - `uv += parallaxOffset;`
7. `vec2 coverUV = getCoverUV(uv, uTextureSize);`
8. If any component of `coverUV` is `< 0.0` or `> 1.0`, `coverUV = clamp(coverUV, 0.0, 1.0);` (clamp to avoid sampling outside the image).
9. `gl_FragColor = texture2D(uTexture, coverUV);`

Net visual result: 35 crisp vertical glass ribbons refracting the portrait, softly faded to the true image at the far-left/far-right ~10% of the width, with the whole field sliding horizontally as the (smoothed) mouse moves off-center — the slide being strongest along the ribbon seams.

## Assets / images
- **1 image sampled at a time**, chosen from **4 interchangeable full-bleed source photos**. Each is **landscape, roughly 16:9** (about 1440×810–1080), and only ever one is wired into `#glassTexture` — the others are swappable alternates that all read well through the ribbons because each has strong tonal or color contrast. No brand logos or text in any of them.
  - **hero image (primary)** — a tightly cropped **editorial fashion shot**: a figure's torso and bent arm in a colorful abstract brushstroke-print garment against a saturated **orange** backdrop; warm skin tone, multicolor cloth (teal, mustard, deep red, navy, cream).
  - **alternate 1** — a minimal **still life**: a single smooth rounded **pebble/pod form** nestled in a soft dimpled surface; near-monochrome **cool grey-white** with gentle shadow gradients.
  - **alternate 2** — a **cinematic space scene**: a lone dark human **silhouette** standing before a huge glowing **orange sun** in a deep **midnight-blue** starfield; very high contrast (bright orange core, dark foreground).
  - **alternate 3** — an abstract **texture**: concentric **spiral grooves** carved into a white plaster/clay surface; near-monochrome **white and pale grey** with fine dark line detail.
- Set the chosen image as the `src` of `#glassTexture`. It is the only asset the shader ever samples; the `<img>` stays `display:none`. High-contrast subjects (the orange fashion crop and the sun silhouette especially) make the vertical glass ribbons read most clearly.

## Behavior notes
- Desktop, pointer-driven. There is no click, scroll, or keyboard interaction; touch devices simply see the centered (un-parallaxed) refraction.
- The loop runs forever at rAF cadence; there is no reduced-motion branch in the original (the motion is subtle and pointer-gated — at rest the image sits still and centered).
- `pixelRatio` is capped at 2 for performance. The canvas always matches the window size.
- Cap the effect to WebGL-capable browsers; if `uTexture` is still null (image not yet decoded) the plane simply renders empty/black until the texture loads.
