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

Tint
#F36527
Ribs13
Tilt-32
Smear0.22
Glow1.10
Speed1.00
Background
#0A0B10

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/undertones

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