Dither Gradient
Installation
npx shadcn@latest add https://uibrik.com/r/dither-gradient.jsonAdd the cn helper
Imports
cnfrom@/lib/utils, whichshadcn initcreates. Add it by hand without shadcn.Copy the source into your project
components/uibrik/dither-gradient/dither-gradient.tsx"use client"; import { ShaderSurface, type ShaderBackgroundProps } from "../_shader-background/ShaderSurface"; // A soft, slowly bending gradient drawn with nothing but the colours you give it: each pixel picks // one of them through an 8x8 ordered (Bayer) dither, so in-between tones become fine patterns. Big // pixels make it retro, small ones make it read as texture. The pointer lifts the tone toward the // last colour, trailing a little behind. const SHADER = /* glsl */ ` uniform float u_pixel; // dither pixel size, css px uniform float u_scale; // size of the shapes uniform float u_warp; // how much the gradient bends uniform float u_angle; // direction the colours run, degrees uniform float u_sweep; // how far apart the first and last colours sit uniform float u_radius; // reach of the pointer light, css px // threshold for an 8x8 Bayer matrix, 0..1; the lowest coordinate bits weigh the most float bayer8(vec2 p) { ivec2 i = ivec2(mod(p, 8.0)); int v = 0; for (int b = 0; b < 3; b++) { int x = (i.x >> b) & 1; int y = (i.y >> b) & 1; v = v * 4 + (((x ^ y) << 1) | y); } return (float(v) + 0.5) / 64.0; } vec3 shade(vec2 frag) { float px = max(1.0, u_pixel * u_dpr); vec2 cell = floor(frag / px); vec2 c = (cell + 0.5) * px; vec2 uv = c / u_res; float aspect = u_res.x / u_res.y; vec2 p = (uv - 0.5) * vec2(aspect, 1.0) * (1.2 / u_scale); float t = u_time * 0.06; vec2 q = vec2(snoise(vec3(p, t)), snoise(vec3(p + 5.2, t))); float f = snoise(vec3(p + q * u_warp * 0.6, t * 0.8)); float a = radians(u_angle); float v = 0.5 + f * 0.45 + dot(uv - 0.5, vec2(cos(a), sin(a))) * u_sweep; float pd = pointerDist(c, 0.5) / (u_radius * u_dpr); v = clamp(v + exp(-pd * pd * 2.0) * u_pointerAmt * 0.35, 0.0, 1.0); float scaled = v * float(u_count - 1); float lo = floor(scaled); int idx = int(lo) + (scaled - lo > bayer8(cell) ? 1 : 0); return oklabToLinear(u_colors[min(idx, u_count - 1)]); } `; const COLORS = ["#0f172a", "#4338ca", "#db2777", "#fbbf24"]; export type DitherGradientProps = Omit<ShaderBackgroundProps, "grain"> & { /** Size of a dither pixel, in px; larger is more retro. */ pixelSize?: number; /** Size of the shapes; larger is calmer. */ scale?: number; /** How much the gradient bends. */ warp?: number; /** Direction the colours run, in degrees; 90 runs bottom to top. */ angle?: number; /** How far apart the first and last colours sit. */ sweep?: number; /** How far the pointer light reaches, in px. */ radius?: number; /** How lazily the light follows the pointer, 0 (locked) to 1 (drifts after it). */ follow?: number; }; export function DitherGradient({ colors = COLORS, speed = 1, animate = true, interactive = true, seed = 0, pixelSize = 3, scale = 1, warp = 1, angle = 90, sweep = 0.6, radius = 180, follow = 0.6, ...rest }: DitherGradientProps) { return ( <ShaderSurface shader={SHADER} uniforms={{ u_pixel: pixelSize, u_scale: scale, u_warp: warp, u_angle: angle, u_sweep: sweep, u_radius: radius }} colors={colors} speed={speed} animate={animate} interactive={interactive} grain={0} seed={seed} follow={follow} maxDpr={2} // CSS angles run clockwise from the top; the shader's run counter-clockwise from the right fallback={() => `linear-gradient(${90 - angle}deg, ${colors.join(", ")})`} {...rest} /> ); }
Adds the files to
components/uibrik/dither-gradient. With the @uibrik namespace set up: npx shadcn@latest add @uibrik/dither-gradient.Props
DitherGradient- colors
- Type string[]
- ["#0f172a","#4338ca","#db2777","#fbbf24"]
- The only colours drawn, in order; 2 to 6.
- pixelSize
- Type number
- 3
- Size of a dither pixel; larger is more retro.
- scale
- Type number
- 1
- Size of the shapes; larger is calmer.
- warp
- Type number
- 1
- How much the gradient bends.
- angle
- Type number
- 90
- Direction the colours run; 90 runs bottom to top.
- sweep
- Type number
- 0.6
- How far apart the first and last colours sit.
- speed
- Type number
- 1
- Animation speed; 1 is the designed pace.
- seed
- Type number
- 0
- Shifts the composition, so two backgrounds with the same settings still differ.
- animate
- Type boolean
- true
- Move on its own. Off draws one still frame.
- interactive
- Type boolean
- true
- React to the pointer.
- radius
- Type number
- 180
- How far the pointer light reaches.
- follow
- Type number
- 0.6
- How lazily the light follows the pointer, 0 (locked) to 1 (drifts after it).
- children
- Type ReactNode
- –
- Content shown on top of the background.
- className
- Type string
- –
- Classes for the box; it fills whatever size you give it.