palette-field@0.1.0
Palette field
Layered fBM read as ink on paper: one field steers another, the result is pigment density between a light ground and a deep ink, and one accent bleeds in only along the contours. Ridged veins add the brush texture; paper grain sits over everything.
Controls
Install
One command copies the composition into your project (with the @vshaders namespace registered in components.json; the plain URL https://vshaders.com/r/palette-field.json works with no setup). npm dependencies install alongside.
npx shadcn@latest add @vshaders/palette-fieldFiles that land in your tree: shaders/palette-field.wgsl, components/palette-field.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/palette-field) 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 { clamp01 } from "@vgpu/wgsl-std/math";
import { hash2 } from "@vgpu/wgsl-std/hash";
import { linearToSrgb3 } from "@vgpu/wgsl-std/color";
Source
The full entry shader, palette-field.wgsl. Copy it and it is yours.
// effect 009 · palette field — layered fBM as ink on paper.
// A painting, not a lightshow: one fBM field slowly steers another, and the
// result is read as pigment density between three inks — paper, a deep ink,
// and one accent that only bleeds in along the contours where the density
// crosses its band. Ridged detail adds the brush texture, OKLab keeps every
// wash tonal instead of neon, and paper grain sits over everything.
// 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 { clamp01 } 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,
// Noise frequency: how many washes fit across the canvas.
scale: f32,
// The paper: the light ground every wash sits on.
colorPaper: vec3f,
// Multiplies time in the drift.
speed: f32,
// The ink: the dark end of the density ramp.
colorInk: vec3f,
// Paper grain amount.
grain: f32,
// The accent: bleeds in along the contour band only.
colorAccent: 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);
let t = uniforms.time * 0.03 * uniforms.speed;
let q = p * max(uniforms.scale, 0.05);
// The warp field steers the density field: strokes follow the flow.
let warp = vec2f(
fbmSimplex2d(q + vec2f(0.0, t * 2.0), 4, 2.0, 0.5),
fbmSimplex2d(q + vec2f(5.2, 1.3 - t * 1.6), 4, 2.0, 0.5),
);
let density = fbmSimplex2d(q + warp * 0.9 + vec2f(t, -t * 0.6), 5, 2.0, 0.5);
// Ridged detail: fine veins where the field folds, the brush texture.
let veins = 1.0 - abs(fbmSimplex2d(q * 2.3 + warp * 1.4 - vec2f(t * 0.8, 0.0), 4, 2.0, 0.5));
// Pigment: paper into ink, eased so most of the canvas stays in the light
// and dark washes pool instead of covering everything.
let wash = clamp01(smoothstep(-0.55, 0.75, density) + veins * veins * 0.18);
var color = mixOklab(uniforms.colorPaper, uniforms.colorInk, wash);
// The accent bleeds in only along the contour band of the density field:
// a narrow window around the mid-tone, like a glaze catching the edges.
let band = smoothstep(0.3, 0.5, wash) * smoothstep(0.78, 0.55, wash);
color = mixOklab(color, uniforms.colorAccent, band * 0.55);
// Deepen the folds a touch so the painting has weight.
color = mixOklab(color, uniforms.colorInk, veins * veins * veins * 0.12);
// Paper grain, in linear light, before encoding.
let g = hash2(position.xy * 0.737).x - 0.5;
color += vec3f(g * uniforms.grain * 0.06);
return vec4f(linearToSrgb3(clamp(color, vec3f(0.0), vec3f(1.0))), 1.0);
}