flow@0.1.0

Flow

Layered fBM with a liquid look: one field steers another, and the pointer stirs the pair. Velocity displaces the warp domain under the cursor, so dragging across the canvas visibly drags the flow. One pass of math, not a simulation.

Controls

Color A
#0070F3
Color B
#00DFD8
Scale1.4
Speed1.00
Stir radius0.30
Stir strength0.35
Background
#070D1D

Install

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

npx shadcn@latest add @vshaders/flow

Files that land in your tree: shaders/flow.wgsl, components/flow.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/flow) for exact values.

Imports

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

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

import { mixOklab } from "@vshaders/color/oklab";

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

Source

The full entry shader, flow.wgsl. Copy it and it is yours.

// effect 010 · flow — pointer-stirred liquid, one pass, no simulation.
// Layered domain-warped fBM: a base field warps a second, finer one, and the
// pointer displaces the warp domain with a velocity bump that falls off
// smoothly around the cursor, so stirring visibly drags the flow.
// Works in linear light; encodes to sRGB once at the end.

import { fbmSimplex2d } from "@vgpu/wgsl-std/noise/simplex";
import { mixOklab } from "@vshaders/color/oklab";
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,
  // Noise frequency: how many field features fit across the card.
  scale: f32,
  // Gradient endpoints, linear light (defaults: #0070f3 blue, #00DFD8 cyan).
  colorA: vec3f,
  // Multiplies time in every animation term.
  speed: f32,
  colorB: vec3f,
  // Radius of the stir bump around the pointer, in centered field units.
  stirRadius: f32,
  // Pointer position, physical pixels; canvas center before any interaction.
  pointer: vec2f,
  // Smoothed pointer velocity, physical pixels per second; zero when idle.
  pointerVelocity: vec2f,
  // How far one field-unit-per-second of pointer speed displaces the domain.
  stirStrength: f32,
  // Ground the effect sinks into at the edges, linear light: expose it so
  // the effect sits on any page, not just this gallery's ink.
  backgroundColor: vec3f,
}

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

@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);
  // The pointer transforms exactly like position; velocity scales by the same
  // pixels-to-field factor (1 / res.y on both axes after the aspect stretch).
  let pointerField = (uniforms.pointer / res - vec2f(0.5)) * vec2f(aspect, 1.0);
  let velocityField = uniforms.pointerVelocity / res.y;
  let t = uniforms.time * 0.08 * uniforms.speed;

  // The stir: displacement proportional to pointer velocity, under a Gaussian
  // falloff so it dies out smoothly past stirRadius.
  let offset = p - pointerField;
  let radius = max(uniforms.stirRadius, 1e-3);
  let falloff = exp(-dot(offset, offset) / (radius * radius));
  let stir = velocityField * uniforms.stirStrength * falloff;

  // Layered warp: the stirred base field steers a finer second octave layer.
  let q = (p - stir) * uniforms.scale;
  let base = fbmSimplex2d(q + vec2f(0.0, t * 1.4), 4, 2.0, 0.5);
  let fine = fbmSimplex2d(q * 1.9 + vec2f(base * 1.2 - t, base * 0.8 + t * 0.7), 4, 2.0, 0.5);
  let band = base * 0.55 + fine * 0.45;

  // Perceptual gradient through the band; a sheen where the layers agree
  // gives the surface its gloss.
  let blend = smoothstep(-0.55, 0.95, band);
  var color = mixOklab(uniforms.colorA, uniforms.colorB, blend);
  let sheen = smoothstep(0.35, 0.85, fine) * smoothstep(0.1, 0.7, base);
  color = mixOklab(color, vec3f(0.75, 0.95, 1.0), sheen * 0.3);

  // Churned regions glow toward white-cyan, so the wake of a stir reads even
  // after the displacement itself is subtle.
  let churn = min(length(stir) * 2.0, 0.6);
  color = mixOklab(color, vec3f(0.85, 0.97, 1.0), churn * 0.5);

  // Sink the edges into the page ink.
  let vignette = smoothstep(1.2, 0.4, length(p));
  color = mixOklab(uniforms.backgroundColor, color, 0.15 + 0.85 * vignette);
  return vec4f(linearToSrgb3(clamp(color, vec3f(0.0), vec3f(1.0))), 1.0);
}