On this site

Pattern Waves

A printed surface of dots that folds like silk and ripples under your pointer.

OGL · GLSL · Ripple simulation

The playground needed a hero that feels tactile rather than cosmic. A lit height field drawn as halftone marks reads like fabric or water, and the cursor leaves a real ripple wake across it.

Two passes on full-screen triangles. The first renders one texel per mark: it samples a moving height field (draped folds, rolling swell or rings), takes its gradient for a normal, lights it, and packs tone, alpha, glint and lift into RGBA. The second pass draws the marks themselves, reading that tiny texture and checking the three rows around each pixel so lifted dots can cross into the next row. The cursor drives a half-float ripple simulation on a coarse grid that is added to the height before shading, and the loop sleeps whenever nothing is moving.

void main() {
  vec2 css = (uOrigin + (floor(gl_FragCoord.xy) + 0.5) * uPitch) / uDpr;
  float e = max(uPitch.y / uDpr, 4.0);
  float h = heightAt(css);
  float hx = (heightAt(css + vec2(e, 0.0)) - heightAt(css - vec2(e, 0.0))) / (2.0 * e);
  float hy = (heightAt(css + vec2(0.0, e)) - heightAt(css - vec2(0.0, e))) / (2.0 * e);
  vec3 n = normalize(vec3(-vec2(hx, hy) * uUnit * uDepth * 0.4, 1.0));

  float diffuse = clamp(dot(n, uLight), 0.0, 1.0);
  vec3 halfway = normalize(uLight + vec3(0.0, 0.0, 1.0));
  float spec = pow(clamp(dot(n, halfway), 0.0, 1.0), 160.0) * uShine * 1.15;
  float tone = clamp(diffuse * 0.78 + spec, 0.0, 1.0);

  // one texel per mark: size, alpha, glint, vertical lift
  fragColor = vec4(pow(tone, 2.2), uOpacity, spec * 1.6, h * uDepth * 0.42 + 0.5);
}