← shader.gallery
Sheen Veil
‹ festoon brume ›
Post-processing

One-click post-FX looks — stack as many as you like. Each card's own sliders fine-tune it.

Embed this background

A one-line web component, loaded from the CDN.

Fragment shader

GLSL ES · MIT · yours to copy

// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 E. T. Carter <[email protected]>
// sheen (Veil) — two layers of sheer woven gauze lie over a dark field, one
// rotated a hair and sliding against the other so their fine thread grids beat
// into broad luminous moiré fringes: wide, soft, curving rivers of brightness
// generated by the fabric itself, with no light source anywhere. Inside each
// bright fringe the micro-weave resolves into visible threads, and successive
// fringe orders take different palette hues. The upper gauze drifts and minutely
// rotates on slow incommensurate sines, so the whole pattern sweeps and re-spaces
// itself — tiny fabric motion amplified into large traveling bands.
//
// Uniforms provided by the runtime:
//   u_time        seconds, monotonically increasing
//   u_resolution  drawing-buffer size in device pixels
//   u_mouse       pointer in device pixels (0,0 when absent)
//   u_pixelRatio  devicePixelRatio used for the buffer
//   u_palette[4]  four glow colours, themeable (linear-ish 0..1 rgb)
precision highp float;

uniform float u_time;
uniform vec2  u_resolution;
uniform vec2  u_mouse;
uniform float u_pixelRatio;
uniform vec3  u_palette[4];

// tweakable params (see meta.json; the runtime feeds defaults)
uniform float u_weave;         // css-px spacing of gauze threads     (default 6)
uniform float u_shimmerSpeed;  // upper-layer drift/rotate speed      (default 0.35)
uniform float u_fringeGlow;    // brightness of the moiré fringes     (default 0.8)

const vec3  BG  = vec3(0.035, 0.035, 0.043); // near-black base ~#09090B
const float TAU = 6.2831853;

// a single sheer woven gauze: product of an x-grating and a y-grating, each a
// soft thread (raised-cosine). Returns ~0 in the open gaps, ~1 on a thread
// crossing. `cells` = thread spacing in pixels; p already in that layer's frame.
float gauze(vec2 p, float cells) {
  vec2 g = 0.5 + 0.5 * cos(p / cells * TAU);   // 0..1 per axis, 1 at thread
  // sharpen each grating into a thin thread, then the weave is their max-ish
  // overlay (threads in either direction read as fabric, crossings brightest)
  g = g * g;
  return max(g.x, g.y) * 0.6 + g.x * g.y * 0.4;
}

// cyclic triangular weight for a palette entry centred at c on a 0..4 wheel
float wheelW(float s, float c) {
  float d = abs(s - c);
  return max(0.0, 1.0 - min(d, 4.0 - d));
}

void main() {
  float pr  = u_pixelRatio;
  vec2  fc  = gl_FragCoord.xy;
  vec2  res = u_resolution;
  vec2  ctr = res * 0.5;
  float t   = u_time;

  vec3 col = BG;

  // guard the weave spacing away from zero so the gratings never blow up
  float refScale = min(u_resolution.x, u_resolution.y) / (max(u_pixelRatio, 1.0) * 400.0);
  float cells = max(u_weave, 0.5) * refScale * pr;   // px between threads in a layer

  // pixel position relative to centre (so rotation pivots on screen centre)
  vec2 q = fc - ctr;

  // --- lower gauze: fixed frame ---
  float lower = gauze(q, cells);

  // --- upper gauze: minutely rotated + drifting on slow incommensurate sines ---
  // tiny angle (a fraction of a degree up to a couple degrees) — this small
  // rotation is what amplifies into broad fringes; it breathes over time.
  // The moiré band period in screen px scales like cells / angle. To honour the
  // spec (finer weave → BROADER, smoother fringes), scale the rotation/drift by
  // (cells/ref)^2 so the band period works out ∝ 1/cells: a finer, denser weave
  // (small u_weave) yields a smaller beat angle and thus wide, smooth fringes,
  // while a coarse weave (large u_weave) cranks the angle into tight, busy ones.
  float sp = u_shimmerSpeed;
  float ref = 6.0 * refScale * pr;                       // reference thread spacing (default)
  float wscale = (cells / ref) * (cells / ref);
  float ang = (0.018 + 0.016 * sin(t * 0.13 * sp + 0.7)
                     + 0.009 * sin(t * 0.077 * sp + 2.1)) * wscale;
  float ca = cos(ang), sa = sin(ang);
  mat2 rot = mat2(ca, -sa, sa, ca);
  // sub-thread drift of the upper layer, also incommensurate — slides the beat.
  // Drift amplitude in screen px also scales with wscale*ref so the beat sweep
  // keeps pace with the band spacing across the weave range.
  vec2 drift = ref * wscale * vec2(
    (0.9 * sin(t * 0.11 * sp) + 0.5 * sin(t * 0.043 * sp + 1.3)),
    (0.8 * sin(t * 0.087 * sp + 2.0) + 0.6 * sin(t * 0.031 * sp))
  );
  vec2 qu = rot * q + drift;
  float upper = gauze(qu, cells);

  // --- moiré: the two weaves multiply; their beat is a slow spatial envelope ---
  // The fine carrier (per-thread) and the broad fringe (the beat) both live in
  // this product. Separate them: `weave` is the fine fabric, `fringe` is the
  // smooth low-frequency interference envelope that paints the luminous rivers.
  float product = lower * upper;

  // broad fringe envelope: phase difference between the two layers' gratings
  // gives the moiré directly, independent of the fine carrier. Because the upper
  // layer is rotated, this phase shift grows with radius and bends, so the beat
  // forms broad CURVING rivers rather than a rigid grid. We sum (not max) the two
  // axis beats so the bands cross softly and never punch hard black holes.
  // displacement of the upper layer relative to the lower at this pixel. Because
  // the upper layer is rotated, this vector grows and swings with radius, so its
  // phase forms broad CURVING rivers — the true moiré, not an axis-aligned grid.
  vec2  disp = (qu - q) / cells;            // in thread units
  // two incommensurate projections of the displacement → smoothly curving bands
  float pa = dot(disp, vec2(0.92, 0.39));   // primary band family (the rivers)
  float pb = dot(disp, vec2(-0.39, 0.92));  // cross family, lower freq = undulation
  float fa = 0.5 + 0.5 * cos(pa * TAU);
  float fb = 0.5 + 0.5 * cos(pb * TAU * 0.5);  // half-freq: gently bows the rivers
  // dominant primary rivers, softly undulated by the cross family so they read as
  // continuous wide curving ribbons rather than a checker of lozenges
  float fringe = fa * (0.78 + 0.22 * fb);
  // crisp the bright rivers; off-fringe field falls toward near-black so the
  // frame reads as luminous interference over darkness
  fringe = smoothstep(0.40, 0.96, fringe);

  // fringe ORDER index: which band are we in, used to pick a palette hue so
  // successive orders read as different tints.
  float order = pa * 0.6 + pb * 0.25 + length(disp) * 0.15;

  // --- palette (headless contexts can leave u_palette zeroed → midnight) ---
  vec3 c0 = u_palette[0], c1 = u_palette[1], c2 = u_palette[2], c3 = u_palette[3];
  if (dot(c0, c0) + dot(c1, c1) + dot(c2, c2) + dot(c3, c3) < 1e-5) {
    c0 = vec3(0.231, 0.510, 0.965); c1 = vec3(0.659, 0.333, 0.969);
    c2 = vec3(0.133, 0.827, 0.933); c3 = vec3(0.957, 0.247, 0.369);
  }

  // hue from fringe order + a gentle time roll (loops, no visible reset)
  float k  = order * 0.5 + t * 0.01 * sp;
  float s  = fract(k) * 4.0;
  float w0 = wheelW(s, 0.0), w1 = wheelW(s, 1.0), w2 = wheelW(s, 2.0), w3 = wheelW(s, 3.0);
  vec3  tint = (c0 * w0 + c1 * w1 + c2 * w2 + c3 * w3) / max(w0 + w1 + w2 + w3, 0.001);

  // brightness = broad fringe envelope, with the fine weave visible inside it.
  // Inside a bright fringe the micro-threads resolve; in the dark gaps near-black.
  float micro = mix(0.45, 1.0, product);        // fabric texture modulation
  float glow  = fringe * micro * u_fringeGlow;

  // a soft secondary so brightest crossings sparkle a touch
  float spark = fringe * product * product * u_fringeGlow * 0.6;

  // radial vignette keeps the frame composed: luminous toward centre, hush at edges
  float vign = 1.0 - smoothstep(0.45, 1.15, length((fc - ctr) / res));

  col += tint * glow * 1.05 * vign;
  col += tint * spark * 0.55 * vign;
  // a whisper of base sheen so the gauze is just suggested in fringe valleys
  // (keeps it from going dead black) — kept low so the rivers dominate
  col += tint * product * (0.03 + 0.02 * fringe) * vign;

  gl_FragColor = vec4(col, 1.0);
}