mesh-gradient@0.1.0

Mesh gradient

Four color spots drifting on their own slow orbits, blended as a weighted OKLab sum so the midpoints stay alive. A center swirl and simplex distortion bend the domain into a silky, organic gradient, and a whisper of grain keeps the long ramps from banding.

Controls

Color A
#272176
Color B
#305DFF
Color C
#F6EACE
Color D
#FF6C4D
Colors4
Distortion0.60
Swirl0.70
Speed1.00
Grain0.40

Install

One command copies the composition into your project (with the @vshaders namespace registered in components.json; the plain URL https://vshaders.com/r/mesh-gradient.json works with no setup). npm dependencies install alongside.

npx shadcn@latest add @vshaders/mesh-gradient

Files that land in your tree: shaders/mesh-gradient.wgsl, components/mesh-gradient.tsx, lib/run-effect.ts, lib/shared-gpu.ts, wgsl-env.d.ts.

The shader imports WGSL modules from npm, resolved by vgpu's loader. In next.config.ts, wire @vgpu/wgsl/loader-webpack for *.wgsl under both turbopack.rules and the webpack() hook, and keep the installed wgsl-env.d.ts at your project root so .wgsl imports typecheck. Loader setup: https://vgpu.sh/docs. Effect gallery and sources: https://vshaders.com. Color params are linear-light RGB, not sRGB hex/255: convert hex through the sRGB transfer function, or use Copy params in the live editor (https://vshaders.com/effects/mesh-gradient) for exact values.

Imports

What this entry shader composes; every module resolves from npm.

import { linearSrgbToOklab, oklabToLinearSrgb } from "@vshaders/color/oklab";

import { simplex2d } from "@vgpu/wgsl-std/noise/simplex";

import { rotate2d } from "@vgpu/wgsl-std/math";

import { hash2 } from "@vgpu/wgsl-std/hash";

import { linearToSrgb3 } from "@vgpu/wgsl-std/color";

Source

The full entry shader, mesh-gradient.wgsl. Copy it and it is yours.

// effect 001 · mesh gradient — flowing color spots under swirl and noise.
// Four color spots drift along their own slow orbits; every pixel blends
// them with inverse-square falloff, mixed as a weighted sum in OKLab so the
// midpoints stay alive instead of graying out. Before sampling, the domain
// is swirled around the center (stronger inside, dying toward the edges)
// and bent by simplex distortion, which is what turns radial blobs into a
// silky, organic gradient. A whisper of grain keeps the long ramps from
// banding. Works in linear light; encodes to sRGB once at the end.

import { linearSrgbToOklab, oklabToLinearSrgb } from "@vshaders/color/oklab";
import { simplex2d } from "@vgpu/wgsl-std/noise/simplex";
import { rotate2d } from "@vgpu/wgsl-std/math";
import { hash2 } from "@vgpu/wgsl-std/hash";
import { linearToSrgb3 } from "@vgpu/wgsl-std/color";

// Tunable members follow resolution/time. Defaults live in lib/effects.ts and
// must be set by the runner: an unset uniform member reads as zero. Members
// are ordered so each vec3f lands on a 16-byte boundary with an f32 in its pad.
struct Uniforms {
  resolution: vec2f,
  time: f32,
  // How far the simplex field bends the sampling domain.
  distortion: f32,
  colorA: vec3f,
  // Rotation of the domain around the center; decays toward the edges.
  swirl: f32,
  colorB: vec3f,
  // Multiplies time in the drift, the swirl, and the distortion.
  speed: f32,
  colorC: vec3f,
  // Film grain amount over the final image.
  grain: f32,
  colorD: vec3f,
  // How many palette colors the spots cycle through, 1 to 4.
  colorCount: f32,
}

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

// Sharpened inverse-power weight of a spot at `site` for the point `p`.
// The exponent keeps each color region holding its own ink, so the gradient
// reads as flowing zones instead of averaging into a single cream.
fn spotWeight(p: vec2f, site: vec2f) -> f32 {
  let d = p - site;
  return 1.0 / pow(dot(d, d) + 0.03, 1.5);
}

@fragment
fn main(@builtin(position) position: vec4f) -> @location(0) vec4f {
  let res = max(uniforms.resolution, vec2f(1.0));
  let uv = position.xy / res;
  let aspect = res.x / res.y;
  let p = (uv - vec2f(0.5)) * vec2f(aspect, 1.0);
  let t = uniforms.time * 0.1 * uniforms.speed;

  // Swirl: rotate around the center, strongest inside, easing off toward
  // the edges — wide and strong enough to twist the colors into arms.
  let radius = length(p);
  let swirlAngle = uniforms.swirl * 3.4 * exp(-radius * 1.1) + t * 0.2;
  var q = rotate2d(p, swirlAngle);

  // Organic distortion at two octaves: the broad field bends the zones, the
  // finer one keeps their boundaries alive.
  let bend = vec2f(
    simplex2d(q * 1.1 + vec2f(t * 0.8, 3.1)) + 0.5 * simplex2d(q * 2.4 - vec2f(t * 1.2, 1.7)),
    simplex2d(q * 1.1 - vec2f(2.7, t * 0.8)) + 0.5 * simplex2d(q * 2.4 + vec2f(1.3, t * 1.2)),
  );
  q += bend * uniforms.distortion * 0.4;

  // The spots: four sites on independent slow orbits.
  let siteA = vec2f(0.62 * cos(t * 0.9), 0.45 * sin(t * 0.7));
  let siteB = vec2f(0.66 * cos(-t * 0.6 + 2.1), 0.5 * sin(t * 0.8 + 1.1));
  let siteC = vec2f(0.55 * cos(t * 0.5 + 4.2), 0.55 * sin(-t * 0.9 + 2.6));
  let siteD = vec2f(0.7 * cos(-t * 0.75 + 1.3), 0.4 * sin(t * 0.65 + 4.4));

  // The spots cycle through the first colorCount palette entries, so the
  // same mesh reads as duotone at 2 and full at 4.
  var palette = array<vec3f, 4>(uniforms.colorA, uniforms.colorB, uniforms.colorC, uniforms.colorD);
  let count = i32(clamp(round(uniforms.colorCount), 1.0, 4.0));
  let wA = spotWeight(q, siteA);
  let wB = spotWeight(q, siteB);
  let wC = spotWeight(q, siteC);
  let wD = spotWeight(q, siteD);
  let wSum = max(wA + wB + wC + wD, 1e-5);

  // Weighted OKLab sum: an N-way perceptual mix, vivid through the middle.
  var lab = linearSrgbToOklab(palette[0 % count]) * wA
    + linearSrgbToOklab(palette[1 % count]) * wB
    + linearSrgbToOklab(palette[2 % count]) * wC
    + linearSrgbToOklab(palette[3 % count]) * wD;
  var color = oklabToLinearSrgb(lab / wSum);

  // Grain, in linear light, before encoding: hides banding on the soft ramps.
  let g = hash2(position.xy * 0.911).x - 0.5;
  color += vec3f(g * uniforms.grain * 0.08);

  return vec4f(linearToSrgb3(clamp(color, vec3f(0.0), vec3f(1.0))), 1.0);
}