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

The item URL works with no components.json setup. Files land in your tree and npm dependencies install alongside; the shader needs vgpu's WGSL loader wired once, in Setup.

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);
}