undertones@0.1.0
Undertones
Light behind ribbed glass: a soft color patch rides the pointer while diagonal ribs smear it into streaked bands, each rib sliding the light by its own hashed offset. Seams darken between ribs and a specular crest shines only where light actually sits behind it.
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/undertones.json works with no setup). npm dependencies install alongside.
npx shadcn@latest add @vshaders/undertonesFiles that land in your tree: shaders/undertones.wgsl, components/undertones.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/undertones) for exact values.
Imports
What this entry shader composes; every module resolves from npm.
import { hash2 } from "@vgpu/wgsl-std/hash";
import { smootherstep } from "@vshaders/ease/shape";
import { mixOklab } from "@vshaders/color/oklab";
import { rotate2d, clamp01 } from "@vgpu/wgsl-std/math";
import { linearToSrgb3, luminance } from "@vgpu/wgsl-std/color";
Source
The full entry shader, undertones.wgsl. Copy it and it is yours.
// effect 008 · undertones — light behind ribbed glass.
// A soft color patch (glued to the pointer) sits behind diagonal glass ribs.
// Each rib smears the light along its own axis by a hashed per-rib offset and
// flattens it toward the rib's center line, so the patch breaks into streaked
// bands; seams darken between ribs and a specular crest rides each one, lit
// by whatever light is behind it. All original.
// Works in linear light; encodes to sRGB once at the end.
import { hash2 } from "@vgpu/wgsl-std/hash";
import { smootherstep } from "@vshaders/ease/shape";
import { mixOklab } from "@vshaders/color/oklab";
import { rotate2d, clamp01 } from "@vgpu/wgsl-std/math";
import { linearToSrgb3, luminance } 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 many ribs fit across the canvas.
ribs: f32,
// The light's color, linear light.
tint: vec3f,
// Rib angle in degrees; 0 is horizontal ribs.
tilt: f32,
// Pointer position, physical pixels; canvas center before any interaction.
pointer: vec2f,
// How far each rib slides the light along itself, in field units.
smear: f32,
// Overall glass brightness: transmission and crest highlights together.
glow: f32,
// Multiplies time in the slow shimmer.
speed: 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;
// The light field behind the glass: a patch elongated along the rib axis at
// `center`, plus a soft halo and a dim offset echo so the dark side of the
// canvas is never entirely empty.
fn lightField(q: vec2f, center: vec2f) -> vec3f {
let d = q - center;
let core = exp(-d.x * d.x / 0.055 - d.y * d.y / 0.014);
let halo = exp(-dot(d, d) / 0.35);
let e = d - vec2f(0.55, 0.28);
let echo = exp(-e.x * e.x / 0.07 - e.y * e.y / 0.03);
var color = uniforms.tint * core * 1.7;
color += mixOklab(uniforms.tint, vec3f(1.0, 0.97, 0.92), 0.6) * halo * 0.10;
color += mixOklab(uniforms.tint, vec3f(0.55, 0.6, 0.9), 0.5) * echo * 0.22;
return color;
}
@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, then into rib space.
let pointerField = (uniforms.pointer / res - vec2f(0.5)) * vec2f(aspect, 1.0);
let angle = uniforms.tilt * 0.017453292;
let q = rotate2d(p, angle);
let lightCenter = rotate2d(pointerField, angle);
let t = uniforms.time * 0.4 * uniforms.speed;
// Rib coordinates: x runs along a rib, y crosses them.
let ribs = max(uniforms.ribs, 1.0);
let across = q.y * ribs + 32.0; // bias keeps rib ids positive for hashing
let ribIndex = floor(across);
let phase = fract(across);
let rand = hash2(vec2f(ribIndex * 0.311 + 7.1, 3.7));
// The glass: slide the light along the rib by a hashed offset plus a slow
// per-rib shimmer, and pull the sample toward the rib's center line so the
// patch flattens into bands. A small per-channel stagger fringes the edges.
let wobble = sin(t + ribIndex * 1.7) * 0.25;
let slide = (rand.x - 0.5) * 2.0 * uniforms.smear + wobble * uniforms.smear;
let bandY = (ribIndex + 0.5 - 32.0) / ribs;
let flatY = mix(q.y, bandY, 0.65);
let split = 0.010 + uniforms.smear * 0.05;
let sampleAt = vec2f(q.x + slide, flatY);
let lightR = lightField(sampleAt - vec2f(split, 0.0), lightCenter);
let lightG = lightField(sampleAt, lightCenter);
let lightB = lightField(sampleAt + vec2f(split, 0.0), lightCenter);
var light = vec3f(lightR.x, lightG.y, lightB.z);
// Uneven transmission: some ribs pass the light nearly whole, others barely
// at all, which breaks the patch into the staggered finger pattern.
light *= 0.25 + 0.75 * rand.y;
// Rib profile: dark seams at the boundaries, a soft body in between, and a
// specular crest that only shines where light actually sits behind it.
let body = smootherstep(0.0, 0.18, phase) * smootherstep(1.0, 0.82, phase);
let shade = 0.10 + 0.90 * body;
let crestArg = (phase - 0.35) / 0.09;
let crest = exp(-crestArg * crestArg);
let energy = clamp01(luminance(light) * 1.6);
var color = light * shade * uniforms.glow;
color += vec3f(1.0, 0.98, 0.95) * crest * energy * energy * uniforms.glow * 0.55;
// Floor and grain: the glass is never pure black, and a whisper of noise
// keeps the dark ramps from banding.
color += uniforms.backgroundColor;
let grain = hash2(position.xy * 0.043).x;
color += vec3f(grain * 0.012 - 0.006);
// Sink the edges into the page ink.
let vignette = smoothstep(1.35, 0.5, length(p));
color = mixOklab(uniforms.backgroundColor * 0.7, color, 0.3 + 0.7 * vignette);
return vec4f(linearToSrgb3(clamp(color, vec3f(0.0), vec3f(1.0))), 1.0);
}