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

Paper
#F0EADD
Ink
#222A3F
Accent
#CE8155
Scale0.9
Speed1.00
Grain0.45

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