Mesh Gradient

Animated mesh gradient with distortion

Quick Start

import { createMeshGradient } from './webgpu-market/mesh-gradient/mesh-gradient';

const gradient = createMeshGradient(device, { format: 'rgba8unorm' });

// Each frame
gradient.render(targetTexture, {
  time: elapsed,
  colors: [
    [0.9, 0.2, 0.3, 1],
    [0.2, 0.5, 0.9, 1],
    [0.1, 0.8, 0.4, 1],
  ],
  distortion: 0.15,
});

gradient.destroy();
Source
// Mesh gradient — flowing composition of color spots with organic distortion
// Ported from paper-design/shaders (MIT)

struct Uniforms {
  time: f32,
  colors_count: f32,
  distortion: f32,
  swirl: f32,
  _pad0: f32,
  _pad1: f32,
  _pad2: f32,
  _pad3: f32,
  colors: array<vec4<f32>, 10>,
}

@group(0) @binding(0) var<uniform> u: Uniforms;

struct VertexOutput {
  @builtin(position) position: vec4<f32>,
  @location(0) uv: vec2<f32>,
}

@vertex
fn vs_main(@builtin(vertex_index) vi: u32) -> VertexOutput {
  var pos = array<vec2<f32>, 6>(
    vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2(-1.0,  1.0),
    vec2(-1.0,  1.0), vec2( 1.0, -1.0), vec2( 1.0,  1.0),
  );
  var uvs = array<vec2<f32>, 6>(
    vec2(0.0, 1.0), vec2(1.0, 1.0), vec2(0.0, 0.0),
    vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(1.0, 0.0),
  );
  var out: VertexOutput;
  out.position = vec4(pos[vi], 0.0, 1.0);
  out.uv = uvs[vi];
  return out;
}

// ── Helpers ─────────────────────────────────────────────────────────────

fn rotate2(uv: vec2<f32>, th: f32) -> vec2<f32> {
  let c = cos(th);
  let s = sin(th);
  return vec2(c * uv.x + s * uv.y, -s * uv.x + c * uv.y);
}

fn get_position(i: i32, t: f32) -> vec2<f32> {
  let fi = f32(i);
  let a = fi * 0.37;
  let b = 0.6 + fract(fi / 3.0) * 0.9;
  let c = 0.8 + fract(f32(i + 1) / 4.0);

  let x = sin(t * b + a);
  let y = cos(t * c + a * 1.5);

  return 0.5 + 0.5 * vec2(x, y);
}

// ── Fragment ────────────────────────────────────────────────────────────

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
  var uv = in.uv;
  uv = uv - 0.5 + 0.5;

  let first_frame_offset = 41.5;
  let t = 0.5 * (u.time + first_frame_offset);

  let radius = smoothstep(0.0, 1.0, length(uv - 0.5));
  let center = 1.0 - radius;

  // Organic distortion — sinusoidal waves modulated by position
  for (var i = 1.0; i <= 2.0; i += 1.0) {
    uv.x += u.distortion * center / i * sin(t + i * 0.4 * smoothstep(0.0, 1.0, uv.y)) * cos(0.2 * t + i * 2.4 * smoothstep(0.0, 1.0, uv.y));
    uv.y += u.distortion * center / i * cos(t + i * 2.0 * smoothstep(0.0, 1.0, uv.x));
  }

  // Swirl distortion — rotational warp increasing toward edges
  var uv_rotated = uv - vec2(0.5);
  let angle = 3.0 * u.swirl * radius;
  uv_rotated = rotate2(uv_rotated, -angle);
  uv_rotated += vec2(0.5);

  // Color blending — inverse-distance weighted mix of animated color spots
  var color = vec3(0.0);
  var opacity = 0.0;
  var total_weight = 0.0;
  let count = i32(u.colors_count);

  for (var i = 0; i < 10; i++) {
    if (i >= count) { break; }

    let pos = get_position(i, t);
    let col = u.colors[i];
    let color_fraction = col.rgb * col.a;
    let opacity_fraction = col.a;

    let dist = length(uv_rotated - pos);
    let dist_pow = pow(dist, 3.5);
    let weight = 1.0 / (dist_pow + 1e-3);

    color += color_fraction * weight;
    opacity += opacity_fraction * weight;
    total_weight += weight;
  }

  color /= max(1e-4, total_weight);
  opacity /= max(1e-4, total_weight);
  opacity = clamp(opacity, 0.0, 1.0);

  return vec4(color, opacity);
}
Documentation

mesh-gradient

Animated mesh gradient with distortion and swirl. Renders up to 10 color spots.

API

createMeshGradient(device, options?)

Creates a mesh gradient renderer.

  • deviceGPUDevice
  • options.format — Output texture format. Default 'rgba8unorm'.

Returns { render, destroy }.

render(target, params?)

Renders the gradient into a GPUTexture.

  • target — The GPUTexture to render into.
  • params.time — Animation time in seconds.
  • params.colors — Array of [r, g, b, a] colors (0–1). Up to 10.
  • params.distortion — Organic wave distortion strength. 0–1, default 0.15.
  • params.swirl — Vortex swirl strength. 0–1, default 0.
Further Reading

Further Reading

Rationale

Mesh gradients (also called "aurora gradients" or "blob gradients") are everywhere in modern design — Apple, Linear, Stripe, Vercel. The effect is simple to describe (blended color spots that drift organically) but non-trivial to implement well on the GPU with smooth blending, distortion, and grain.

This module gives you a single-file implementation you can drop into any WebGPU project. Useful for backgrounds, hero sections, loading screens, or generative art.

Original Source

How It Works

The shader places N color spots at animated positions (sinusoidal paths). For each pixel:

  1. Compute the inverse-distance weight to every color spot (1 / dist^3.5).
  2. Blend all colors by their weights (weighted average).
  3. Optionally apply organic distortion (sinusoidal UV warping) and vortex swirl (rotational warp that increases toward edges).
  4. Optionally overlay film grain using value noise.

The inverse-distance weighting is the same principle used in Shepard's interpolation (IDW), a technique from spatial statistics.

Further Learning