# WebGL Interactive Gradient

## Goal
Build a full-screen hero whose entire background is a **living, flowing multi-color gradient rendered in WebGL** (Three.js + two custom fragment shaders). A **real-time fluid simulation** runs in a ping-pong pair of float render targets; a display pass warps a time-evolving trig-based gradient by the fluid's velocity field. **Moving the mouse stirs the fluid** — the gradient ripples, smears and swirls along the cursor's trail, then slowly relaxes back to its ambient flow. A minimal white nav, a centered logo image and a footer strip float on top. The star effect is the cursor-reactive fluid distortion of the animated gradient.

## Tech
Vanilla HTML/CSS/JS with ES module imports. **No GSAP is needed** — all motion is a raw `requestAnimationFrame` loop driving shader uniforms. Use `three` (npm):
```js
import * as THREE from "three";
import { vertexShader, fluidShader, displayShader } from "./shaders.js";
```
Put the three shader source strings in a sibling module `shaders.js` and export them as template-literal strings.

## Layout / HTML
```html
<nav>
  <div class="logo"><p>Orbit Studio</p></div>
  <div class="nav-items">
    <p>Index</p><p>Portfolio</p><p>Info</p><p>Contact</p>
  </div>
</nav>
<section class="hero">
  <div class="gradient-canvas"></div>
  <div class="hero-logo"><img src="/path/logo.png" alt="" /></div>
  <div class="hero-footer">
    <p>Experiment 0469</p>
    <p>Built by Orbit Studio</p>
  </div>
</section>
<script type="module" src="./script.js"></script>
```
- `.gradient-canvas` is the WebGL container — JS appends the renderer's `<canvas>` into it.
- `.hero-logo` holds a single centered logo image on top of the gradient.
- Nav/footer copy is neutral demo text ("Orbit Studio", "Experiment 0469", etc.) — invent your own if you like.

## Styling
Font import: `@import url("https://fonts.googleapis.com/css2?family=Host+Grotesk:ital,wght@0,300..800;1,300..800&display=swap");`

Global reset: `* { margin:0; padding:0; box-sizing:border-box; }` and `body { font-family:"Host Grotesk"; }`

- `img`: `width:100%; height:100%; object-fit:cover;`
- `p`: `color:#fff; font-size:0.9rem; font-weight:450;` — all UI text is small white grotesk.
- `nav, .hero-footer`: `position:absolute; left:0; width:100vw; padding:2rem; display:flex; justify-content:space-between; align-items:center; z-index:2;` (nav sticks to the top by default flow; footer adds `bottom:0`).
- `.nav-items`: `display:flex; gap:4rem;`
- `nav .logo p`: `font-weight:700;`
- `section` (the hero): `position:relative; width:100vw; height:100svh; overflow:hidden;`
- `.hero-logo`: `position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:25%;` — the logo spans 25% of the viewport width, dead center.
- `.gradient-canvas`: `position:absolute; top:0; left:0; width:100%; height:100%;`
- Responsive: `@media (max-width:1000px) { nav { flex-direction:column; gap:2rem; } }`

## The effect (Three.js fluid-distorted gradient — be exact)

### Config object (single source of truth, re-synced to uniforms every frame)
```js
const config = {
  brushSize: 25.0,
  brushStrength: 0.5,
  distortionAmount: 2.5,
  fluidDecay: 0.98,      // how long fluid velocity persists (per-frame multiplier)
  trailLength: 0.8,      // per-frame multiplier on the trail/pressure channel
  stopDecay: 0.85,       // extra damping near a stationary cursor
  color1: "#b8fff7",     // pale mint
  color2: "#6e3466",     // muted plum
  color3: "#0133ff",     // vivid electric blue
  color4: "#66d1fe",     // light sky blue
  colorIntensity: 1.0,
  softness: 1.0,
};
```
Add a `hexToRgb(hex)` helper returning `[r,g,b]` in 0–1 for the color uniforms.

### Scene setup
- `camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);` — full-screen quad rig, no perspective.
- `renderer = new THREE.WebGLRenderer({ antialias: true });` sized to `window.innerWidth × window.innerHeight`, appended into `.gradient-canvas`.
- **Two ping-pong render targets** (`fluidTarget1`, `fluidTarget2`), both `window.innerWidth × window.innerHeight` with:
  ```js
  { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter,
    format: THREE.RGBAFormat, type: THREE.FloatType }
  ```
  `FloatType` is required — the sim stores signed velocities.
- Track `currentFluidTarget` / `previousFluidTarget` refs plus a `frameCount` integer (starts 0).
- `geometry = new THREE.PlaneGeometry(2, 2);` shared by two meshes: `fluidPlane` (with `fluidMaterial`) and `displayPlane` (with `displayMaterial`). No scene graph needed — render each mesh directly with `renderer.render(mesh, camera)`.

**`fluidMaterial` (ShaderMaterial) uniforms:** `iTime` (float), `iResolution` (Vector2 = window size), `iMouse` (Vector4 = x, y, prevX, prevY, initialized 0,0,0,0), `iFrame` (int), `iPreviousFrame` (sampler, starts null), `uBrushSize`, `uBrushStrength`, `uFluidDecay`, `uTrailLength`, `uStopDecay` — all seeded from `config`. Vertex = `vertexShader`, fragment = `fluidShader`.

**`displayMaterial` (ShaderMaterial) uniforms:** `iTime`, `iResolution`, `iFluid` (sampler, starts null), `uDistortionAmount`, `uColor1`–`uColor4` (Vector3s from `hexToRgb`), `uColorIntensity`, `uSoftness`. Vertex = `vertexShader`, fragment = `displayShader`.

### Mouse input
```js
let mouseX = 0, mouseY = 0, prevMouseX = 0, prevMouseY = 0, lastMoveTime = 0;

document.addEventListener("mousemove", (e) => {
  const rect = gradientCanvas.getBoundingClientRect();
  prevMouseX = mouseX;  prevMouseY = mouseY;
  mouseX = e.clientX - rect.left;
  mouseY = rect.height - (e.clientY - rect.top);   // flip Y to GL coords
  lastMoveTime = performance.now();
  fluidMaterial.uniforms.iMouse.value.set(mouseX, mouseY, prevMouseX, prevMouseY);
});
document.addEventListener("mouseleave", () => {
  fluidMaterial.uniforms.iMouse.value.set(0, 0, 0, 0);
});
```
Key detail: the shader treats `iMouse.z > 0.0` as "brush active", so zeroing the vector turns the brush off.

### Render loop (rAF, ping-pong)
Every frame, in this exact order:
1. `time = performance.now() * 0.001;` → write to `iTime` on **both** materials; write `frameCount` to `iFrame`.
2. **Idle cutoff:** if `performance.now() - lastMoveTime > 100` (ms), set `iMouse` to `(0,0,0,0)` — the brush only injects while the mouse is actively moving.
3. Copy every `config` value back into its uniform (brush, decay, distortion, intensity, softness, and re-run `hexToRgb` on the four colors) — this makes the config live-tweakable.
4. **Fluid pass:** `fluidMaterial.uniforms.iPreviousFrame.value = previousFluidTarget.texture;` → `renderer.setRenderTarget(currentFluidTarget);` → `renderer.render(fluidPlane, camera);`
5. **Display pass:** `displayMaterial.uniforms.iFluid.value = currentFluidTarget.texture;` → `renderer.setRenderTarget(null);` → `renderer.render(displayPlane, camera);`
6. **Swap** `currentFluidTarget` ↔ `previousFluidTarget`; `frameCount++`.

### Resize
On `window.resize`: `renderer.setSize(w, h)`, update `iResolution` on both materials, `setSize(w, h)` on **both** render targets, and reset `frameCount = 0` (re-seeds the simulation from frame zero).

## Shaders (`shaders.js`)

**Vertex shader** (shared passthrough):
```glsl
varying vec2 vUv;
void main() {
  vUv = uv;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
```

**`fluidShader`** — a compact self-advecting fluid sim. State per texel (RGBA): `xy` = velocity, `z` = pressure/trail, `w` = misc. Uniforms as listed above; `varying vec2 vUv`.

Helpers (globals `vec2 ur, U;`):
```glsl
// distance from point p to segment a–b
float ln(vec2 p, vec2 a, vec2 b) {
  return length(p-a-(b-a)*clamp(dot(p-a,b-a)/dot(b-a,b-a),0.,1.));
}
// wrap-around samples of the previous frame, in pixel space
vec4 t(vec2 v, int a, int b) { return texture2D(iPreviousFrame, fract((v+vec2(float(a),float(b)))/ur)); }
vec4 t(vec2 v)               { return texture2D(iPreviousFrame, fract(v/ur)); }
// triangle area via Heron's formula
float area(vec2 a, vec2 b, vec2 c) {
  float A = length(b-c), B = length(c-a), C = length(a-b), s = 0.5*(A+B+C);
  return sqrt(s*(s-A)*(s-B)*(s-C));
}
```
`main()`, with `U = vUv * iResolution; ur = iResolution.xy;`:
1. **Seed frame** — `if (iFrame < 1)`: `float w = 0.5+sin(0.2*U.x)*0.5; float q = length(U-0.5*ur); gl_FragColor = vec4(0.1*exp(-0.001*q*q), 0, 0, w);` (a soft central x-velocity bump so the gradient isn't static at load).
2. **Semi-Lagrangian advection** — take `v = U` and four diagonal probes `A=v+(1,1), B=v+(1,-1), C=v+(-1,1), D=v+(-1,-1)`; iterate **8 times**: subtract `t(p).xy` from each of the five points (walk each point back along the velocity field).
3. **Diffusion** — sample `me = t(v)` and its 4-neighborhood `n,e,s,w` (offsets ±1); `me = mix(t(v), 0.25*(n+e+s+w), vec4(0.15, 0.15, 0.95, 0.));` — velocity blends 15% toward the neighbor average, pressure 95%.
4. **Divergence → pressure:** `me.z -= 0.01*((area(A,B,C)+area(B,C,D)) - 4.);`
5. **Pressure gradient → velocity:** `vec4 pr = vec4(e.z, w.z, n.z, s.z); me.xy += 100.*vec2(pr.x-pr.y, pr.z-pr.w)/ur;`
6. **Decay:** `me.xy *= uFluidDecay;` `me.z *= uTrailLength;`
7. **Mouse brush** — only `if (iMouse.z > 0.0)`:
   - `mouseVel = iMouse.xy - iMouse.zw;` `velMagnitude = length(mouseVel);`
   - `q = ln(U, iMouse.xy, iMouse.zw);` (distance to the cursor's segment this frame)
   - clamp the injected vector: `if (l > 0.0) m = min(l, 10.0) * m / l;`
   - `float brushSizeFactor = 1e-4 / uBrushSize; float strengthFactor = 0.03 * uBrushStrength;`
   - `float falloff = pow(exp(-brushSizeFactor*q*q*q), 0.5);` (cubic-exponential falloff → soft wide brush)
   - `me.xyw += strengthFactor * falloff * vec3(m, 10.);`
   - **Stationary-cursor damping** — `if (velMagnitude < 2.0)`: `float influence = exp(-length(U - iMouse.xy) * 0.01); float cursorDecay = mix(1.0, uStopDecay, influence); me.xy *= cursorDecay; me.z *= cursorDecay;` (fluid calms down under a resting cursor instead of buzzing).
8. `gl_FragColor = clamp(me, -0.4, 0.4);` — hard-clamp keeps the sim stable.

**`displayShader`** — the visible gradient, warped by the fluid:
```glsl
void main() {
  vec2 fragCoord = vUv * iResolution;
  vec4 fluid = texture2D(iFluid, vUv);
  vec2 fluidVel = fluid.xy;

  float mr = min(iResolution.x, iResolution.y);
  vec2 uv = (fragCoord * 2.0 - iResolution.xy) / mr;   // centered, aspect-corrected

  uv += fluidVel * (0.5 * uDistortionAmount);          // THE hook: fluid warps the gradient

  float d = -iTime * 0.5;
  float a = 0.0;
  for (float i = 0.0; i < 8.0; ++i) {                  // iterative trig feedback → organic flow
    a += cos(i - d - a * uv.x);
    d += sin(uv.y * i + a);
  }
  d += iTime * 0.5;

  float mixer1 = cos(uv.x * d) * 0.5 + 0.5;
  float mixer2 = cos(uv.y * a) * 0.5 + 0.5;
  float mixer3 = sin(d + a) * 0.5 + 0.5;

  float smoothAmount = clamp(uSoftness * 0.1, 0.0, 0.9);
  mixer1 = mix(mixer1, 0.5, smoothAmount);             // softness pulls bands toward mid-blends
  mixer2 = mix(mixer2, 0.5, smoothAmount);
  mixer3 = mix(mixer3, 0.5, smoothAmount);

  vec3 col = mix(uColor1, uColor2, mixer1);
  col = mix(col, uColor3, mixer2);
  col = mix(col, uColor4, mixer3 * 0.4);               // color4 is only a 40% accent
  col *= uColorIntensity;
  gl_FragColor = vec4(col, 1.0);
}
```

## Assets / images
- **1 logo image**, roughly **square (~1:1)** — a **white abstract logo mark / wordmark** meant to sit centered over the gradient (it renders at 25% of the viewport width). Transparent or near-black background; no real brand.

## Behavior notes
- The gradient **animates on its own from frame one** (the `iTime`-driven trig loop flows continuously); the mouse only *adds* fluid energy on top. With no interaction it settles into a slow ambient drift.
- Mouse input stops injecting after **100ms** without movement, and the fluid decays (`fluidDecay 0.98` / `trailLength 0.8` per frame), so trails linger ~a second then dissolve.
- Desktop-first: the effect is mouse-driven; on touch it just plays the ambient gradient. `FloatType` render targets make it GPU-heavy — no pixel-ratio tricks in the original.
- Resizing rebuilds nothing; it resizes the targets and re-seeds the sim (`frameCount = 0`).
- No GSAP, no scroll behavior, no reduced-motion branch.
