---
title: Animated Grain
description: A high-performance WebGL procedural Bayer dither wave background component. Features pixel quantization, multi-harmonic domain warping, dynamic cursor interaction, analog film grain, and light and dark mode theme switching.
---
## Installation
CLI
Manual
```bash
npx lightswind@latest add animated-grain
```
Copy and paste the following code into your project.
```tsx
"use client";
import React, { useEffect, useRef, useState } from "react";
import { useTheme } from "next-themes";
import { cn } from "@/components/lib/utils";
export type GrainVariant =
| "ocean"
| "violet"
| "cyberpunk"
| "emerald"
| "solar"
| "monochrome"
| "sunset"
| "matrix"
| "custom";
export type DitherPattern = "hash" | "crosshatch" | "bayer";
export interface AnimatedGrainProps {
className?: string;
children?: React.ReactNode;
/** Preset color theme variant */
variant?: GrainVariant;
/** Dither pattern style ("hash" for '#' weave grid, "crosshatch", or "bayer") */
ditherPattern?: DitherPattern;
/** Wave oscillation speed multiplier (default: 1.0) */
speed?: number;
/** Wave density and zoom scale (default: 3.5) */
scale?: number;
/** Domain warp turbulence intensity (default: 2.2) */
intensity?: number;
/** Pixel quantization dither scale factor (0.2 to 2.0, default: 0.6) */
ditherScale?: number;
/** Extra subtle analog film grain overlay (0.0 to 1.0, default: 0.15) */
grainAmount?: number;
/** Color vibrancy & contrast multiplier (default: 1.2) */
contrast?: number;
/** Field rotation angle in degrees (default: 0) */
rotationAngle?: number;
/** Dark corner vignette strength (0.0 to 1.0, auto-adapts by theme if undefined) */
vignetteStrength?: number;
/** Overall opacity (0.0 to 1.0, default: 1.0) */
opacity?: number;
/** Array of up to 4 HEX color strings for custom palette */
colors?: string[];
/** Individual HEX color overrides */
primaryColor?: string;
secondaryColor?: string;
tertiaryColor?: string;
highlightColor?: string;
/** Whether the wave shader responds to cursor movement (default: true) */
interactive?: boolean;
}
/** Variant Color Palettes for Light and Dark Modes */
const VARIANT_PALETTES: Record<
Exclude,
{
light: [string, string, string, string]; // [primary, secondary, tertiary, highlight]
dark: [string, string, string, string];
}
> = {
ocean: {
light: ["#0284c7", "#06b6d4", "#f0f9ff", "#ffffff"],
dark: ["#38bdf8", "#0284c7", "#031221", "#bae6fd"],
},
violet: {
light: ["#5227ff", "#9333ea", "#f5f3ff", "#ffffff"],
dark: ["#ffffff", "#5227ff", "#0a0a0f", "#a289ff"],
},
cyberpunk: {
light: ["#06b6d4", "#d946ef", "#fdf4ff", "#ffffff"],
dark: ["#00f5ff", "#ff007f", "#09090e", "#ffe600"],
},
emerald: {
light: ["#059669", "#10b981", "#f0fdf4", "#ffffff"],
dark: ["#34d399", "#059669", "#04140d", "#a7f3d0"],
},
solar: {
light: ["#ea580c", "#f59e0b", "#fffbeb", "#ffffff"],
dark: ["#fde047", "#f97316", "#140702", "#fed7aa"],
},
monochrome: {
light: ["#1e293b", "#64748b", "#f8fafc", "#ffffff"],
dark: ["#ffffff", "#71717a", "#09090b", "#d4d4d8"],
},
sunset: {
light: ["#e11d48", "#f97316", "#fff1f2", "#ffffff"],
dark: ["#fb7185", "#e11d48", "#18040a", "#fecdd3"],
},
matrix: {
light: ["#16a34a", "#22c55e", "#f0fdf4", "#ffffff"],
dark: ["#4ade80", "#16a34a", "#021206", "#86efac"],
},
};
/** Converts HEX color string ("#5227FF") to RGB float array ([0.32, 0.15, 1.0]) */
function hexToRgb(hex: string): [number, number, number] {
let c = hex.replace("#", "").trim();
if (c.length === 3) {
c = c.split("").map((x) => x + x).join("");
}
const num = parseInt(c, 16);
if (isNaN(num)) return [1, 1, 1];
return [((num >> 16) & 255) / 255, ((num >> 8) & 255) / 255, (num & 255) / 255];
}
const VERTEX_SHADER = `
attribute vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
`;
const FRAGMENT_SHADER = `
precision highp float;
uniform vec2 u_resolution;
uniform float u_time;
uniform vec2 u_mouse;
uniform float u_speed;
uniform float u_intensity;
uniform float u_scale;
uniform float u_downScale;
uniform float u_contrast;
uniform float u_grain;
uniform float u_vignette;
uniform float u_angle;
uniform float u_opacity;
uniform int u_ditherStyle;
uniform vec3 u_primaryColor;
uniform vec3 u_secondaryColor;
uniform vec3 u_tertiaryColor;
uniform vec3 u_highlightColor;
// Procedural Hash (#) Weave Dithering Matrix
// Generates intersecting vertical and horizontal lines forming sharp '#' lattice crosshatching
float hashGridDither(vec2 uv) {
int x = int(mod(uv.x, 4.0));
int y = int(mod(uv.y, 4.0));
// Check if on horizontal line (y=1 or y=3) or vertical line (x=1 or x=3)
bool isVert = (x == 1 || x == 3);
bool isHoriz = (y == 1 || y == 3);
// Intersection points of the '#' grid (4 intersection nodes)
if (isVert && isHoriz) {
int idx = (x == 1 ? 0 : 1) + (y == 1 ? 0 : 2);
if (idx == 0) return 0.00;
if (idx == 1) return 0.08;
if (idx == 2) return 0.16;
return 0.24;
}
// Vertical line segments connecting the '#'
if (isVert) {
int idx = (x == 1 ? 0 : 1) + (y == 0 ? 0 : 2);
if (idx == 0) return 0.32;
if (idx == 1) return 0.40;
if (idx == 2) return 0.48;
return 0.56;
}
// Horizontal line segments connecting the '#'
if (isHoriz) {
int idx = (x == 0 ? 0 : 1) + (y == 1 ? 0 : 2);
if (idx == 0) return 0.36;
if (idx == 1) return 0.44;
if (idx == 2) return 0.52;
return 0.60;
}
// Outer open spaces surrounding the '#' grid
int idx = (x == 0 ? 0 : 1) + (y == 0 ? 0 : 2);
if (idx == 0) return 0.76;
if (idx == 1) return 0.84;
if (idx == 2) return 0.92;
return 0.98;
}
// Procedural Continuous Crosshatch Weave Matrix
float crosshatchDither(vec2 uv) {
int x = int(mod(uv.x, 4.0));
int y = int(mod(uv.y, 4.0));
if ((x == 1 || x == 3) && (y == 1 || y == 3)) return 0.05;
if (x == 1 || x == 3) return 0.35;
if (y == 1 || y == 3) return 0.55;
return 0.90;
}
// 4x4 Standard Bayer Dithering Matrix
float bayer4x4(vec2 uv) {
int x = int(mod(uv.x, 4.0));
int y = int(mod(uv.y, 4.0));
int index = x + y * 4;
if (index == 0) return 0.0 / 16.0;
if (index == 1) return 8.0 / 16.0;
if (index == 2) return 2.0 / 16.0;
if (index == 3) return 10.0 / 16.0;
if (index == 4) return 12.0 / 16.0;
if (index == 5) return 4.0 / 16.0;
if (index == 6) return 14.0 / 16.0;
if (index == 7) return 6.0 / 16.0;
if (index == 8) return 3.0 / 16.0;
if (index == 9) return 11.0 / 16.0;
if (index == 10) return 1.0 / 16.0;
if (index == 11) return 9.0 / 16.0;
if (index == 12) return 15.0 / 16.0;
if (index == 13) return 7.0 / 16.0;
if (index == 14) return 13.0 / 16.0;
return 5.0 / 16.0;
}
// Pseudo-random noise hash
float hash1(vec2 p) {
return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
}
void main() {
vec2 res = max(u_resolution, vec2(1.0));
vec2 fc = gl_FragCoord.xy;
vec2 st = (fc - 0.5 * res) / min(res.x, res.y);
// Rotation transformation
float rad = u_angle * (3.14159265 / 180.0);
mat2 rot = mat2(cos(rad), -sin(rad), sin(rad), cos(rad));
st = rot * st;
// Interactive mouse warp displacement
vec2 mouseST = (u_mouse - vec2(0.5, 0.5)) * vec2(res.x / min(res.x, res.y), res.y / min(res.x, res.y));
mouseST = rot * mouseST;
float mouseDist = length(st - mouseST);
float mouseInfluence = smoothstep(0.9, 0.0, mouseDist);
vec2 mouseWarp = (st - mouseST) * mouseInfluence * 0.35;
vec2 p = (st + mouseWarp) * (u_scale * 0.85);
float t = u_time * u_speed * 0.45;
// Multi-Harmonic Wave & Domain Warping
float wave1 = sin(p.x * 2.0 + t) * cos(p.y * 2.0 + t);
float wave2 = sin(p.x * 3.0 - t * 0.85 + wave1 * u_intensity);
float wave3 = cos(p.y * 4.0 + t * 0.6 + wave2 * u_intensity);
float wave4 = sin((p.x + p.y) * 2.5 + t * 0.4 + wave3 * (u_intensity * 0.5));
float wavePattern = clamp((wave1 + wave2 + wave3 + wave4) * 0.25 + 0.5, 0.0, 1.0);
wavePattern = pow(wavePattern, 1.0 / max(0.2, u_contrast));
// Pixel Dithering via Hash (#) or Crosshatch Matrix lookup
float pixelSize = max(1.0, 1.0 / max(0.05, u_downScale));
vec2 pixelCoord = floor(fc / pixelSize);
float ditherThreshold;
if (u_ditherStyle == 0) {
ditherThreshold = hashGridDither(pixelCoord);
} else if (u_ditherStyle == 1) {
ditherThreshold = crosshatchDither(pixelCoord);
} else {
ditherThreshold = bayer4x4(pixelCoord);
}
// Dither step thresholding
float dithered = step(ditherThreshold, wavePattern);
// Multi-stop harmonic gradient blend
vec3 grad = mix(u_tertiaryColor, u_secondaryColor, smoothstep(0.05, 0.65, wavePattern));
grad = mix(grad, u_primaryColor, smoothstep(0.40, 0.88, wavePattern));
grad = mix(grad, u_highlightColor, smoothstep(0.85, 1.00, wavePattern));
vec3 finalColor = mix(u_tertiaryColor, grad, dithered);
// Subtle edge vignette
float d = length(st * vec2(0.8, 0.55));
finalColor *= mix(1.0, 1.0 - smoothstep(0.2, 1.4, d), clamp(u_vignette, 0.0, 1.0));
// Subtle analog grain layer
float grain = hash1(floor(fc * 1.5) + fract(u_time));
finalColor += (grain - 0.5) * clamp(u_grain, 0.0, 1.0) * 0.25;
gl_FragColor = vec4(clamp(finalColor, 0.0, 1.0), u_opacity);
}
`;
/**
* AnimatedGrain
*
* A high-performance WebGL procedural Hash (#) dither wave background component.
* Features customizable '#' hash lattice weave dithering, domain warping, dynamic cursor interaction, and light/dark themes.
*/
export function AnimatedGrain({
className,
children,
variant = "ocean",
ditherPattern = "hash",
speed = 1.0,
scale = 3.5,
intensity = 2.2,
ditherScale = 0.6,
grainAmount = 0.15,
contrast = 1.2,
rotationAngle = 0,
vignetteStrength,
opacity = 1.0,
colors,
primaryColor,
secondaryColor,
tertiaryColor,
highlightColor,
interactive = true,
}: AnimatedGrainProps) {
const canvasRef = useRef(null);
const mouseRef = useRef({ x: 0.5, y: 0.5, targetX: 0.5, targetY: 0.5 });
const { resolvedTheme, theme } = useTheme();
const [mounted, setMounted] = useState(false);
const [isLightMode, setIsLightMode] = useState(false);
useEffect(() => {
setMounted(true);
const checkIsLight = () => {
if (typeof document !== "undefined") {
if (document.documentElement.classList.contains("dark")) return false;
if (document.documentElement.classList.contains("light")) return true;
}
if (resolvedTheme) return resolvedTheme === "light";
if (theme) return theme === "light";
if (typeof window !== "undefined" && window.matchMedia) {
return !window.matchMedia("(prefers-color-scheme: dark)").matches;
}
return false;
};
setIsLightMode(checkIsLight());
if (typeof document !== "undefined") {
const observer = new MutationObserver(() => {
setIsLightMode(checkIsLight());
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
return () => observer.disconnect();
}
}, [resolvedTheme, theme]);
const paletteKey = mounted && isLightMode ? "light" : "dark";
const selectedPalette = VARIANT_PALETTES[variant === "custom" ? "ocean" : variant][paletteKey];
const activePrimary = primaryColor ?? colors?.[0] ?? selectedPalette[0];
const activeSecondary = secondaryColor ?? colors?.[1] ?? selectedPalette[1];
const activeTertiary = tertiaryColor ?? colors?.[2] ?? selectedPalette[2];
const activeHighlight = highlightColor ?? colors?.[3] ?? selectedPalette[3];
const activeVignette = vignetteStrength ?? (mounted && isLightMode ? 0.0 : 0.4);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const gl =
canvas.getContext("webgl") ||
(canvas.getContext("experimental-webgl") as WebGLRenderingContext | null);
if (!gl) return;
const createShader = (type: number, source: string) => {
const shader = gl.createShader(type);
if (!shader) return null;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.warn("[AnimatedGrain] Shader error:", gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
};
const vert = createShader(gl.VERTEX_SHADER, VERTEX_SHADER);
const frag = createShader(gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
if (!vert || !frag) return;
const program = gl.createProgram();
if (!program) return;
gl.attachShader(program, vert);
gl.attachShader(program, frag);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.warn("[AnimatedGrain] Program link error:", gl.getProgramInfoLog(program));
return;
}
gl.useProgram(program);
// Fullscreen Quad
const quad = new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW);
const posAttr = gl.getAttribLocation(program, "position");
gl.enableVertexAttribArray(posAttr);
gl.vertexAttribPointer(posAttr, 2, gl.FLOAT, false, 0, 0);
const uniforms = {
resolution: gl.getUniformLocation(program, "u_resolution"),
time: gl.getUniformLocation(program, "u_time"),
mouse: gl.getUniformLocation(program, "u_mouse"),
speed: gl.getUniformLocation(program, "u_speed"),
intensity: gl.getUniformLocation(program, "u_intensity"),
scale: gl.getUniformLocation(program, "u_scale"),
downScale: gl.getUniformLocation(program, "u_downScale"),
contrast: gl.getUniformLocation(program, "u_contrast"),
grain: gl.getUniformLocation(program, "u_grain"),
vignette: gl.getUniformLocation(program, "u_vignette"),
angle: gl.getUniformLocation(program, "u_angle"),
opacity: gl.getUniformLocation(program, "u_opacity"),
ditherStyle: gl.getUniformLocation(program, "u_ditherStyle"),
primaryColor: gl.getUniformLocation(program, "u_primaryColor"),
secondaryColor: gl.getUniformLocation(program, "u_secondaryColor"),
tertiaryColor: gl.getUniformLocation(program, "u_tertiaryColor"),
highlightColor: gl.getUniformLocation(program, "u_highlightColor"),
};
let animationFrameId: number;
const startTime = performance.now();
const render = (now: number) => {
if (!canvas || !gl) return;
const dpr = Math.min(typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1, 2);
const displayWidth = Math.floor(canvas.clientWidth * dpr);
const displayHeight = Math.floor(canvas.clientHeight * dpr);
if (canvas.width !== displayWidth || canvas.height !== displayHeight) {
canvas.width = displayWidth;
canvas.height = displayHeight;
gl.viewport(0, 0, displayWidth, displayHeight);
}
// Smooth mouse lerp
if (interactive) {
mouseRef.current.x += (mouseRef.current.targetX - mouseRef.current.x) * 0.06;
mouseRef.current.y += (mouseRef.current.targetY - mouseRef.current.y) * 0.06;
}
const pCol = hexToRgb(activePrimary);
const sCol = hexToRgb(activeSecondary);
const tCol = hexToRgb(activeTertiary);
const hCol = hexToRgb(activeHighlight);
const styleIndex = ditherPattern === "crosshatch" ? 1 : ditherPattern === "bayer" ? 2 : 0;
gl.uniform2f(uniforms.resolution, canvas.width, canvas.height);
gl.uniform1f(uniforms.time, (now - startTime) * 0.001);
gl.uniform2f(uniforms.mouse, mouseRef.current.x, mouseRef.current.y);
gl.uniform1f(uniforms.speed, speed);
gl.uniform1f(uniforms.intensity, intensity);
gl.uniform1f(uniforms.scale, scale);
gl.uniform1f(uniforms.downScale, ditherScale);
gl.uniform1f(uniforms.contrast, contrast);
gl.uniform1f(uniforms.grain, grainAmount);
gl.uniform1f(uniforms.vignette, activeVignette);
gl.uniform1f(uniforms.angle, rotationAngle);
gl.uniform1f(uniforms.opacity, opacity);
gl.uniform1i(uniforms.ditherStyle, styleIndex);
gl.uniform3f(uniforms.primaryColor, pCol[0], pCol[1], pCol[2]);
gl.uniform3f(uniforms.secondaryColor, sCol[0], sCol[1], sCol[2]);
gl.uniform3f(uniforms.tertiaryColor, tCol[0], tCol[1], tCol[2]);
gl.uniform3f(uniforms.highlightColor, hCol[0], hCol[1], hCol[2]);
gl.drawArrays(gl.TRIANGLES, 0, 6);
animationFrameId = requestAnimationFrame(render);
};
animationFrameId = requestAnimationFrame(render);
return () => {
cancelAnimationFrame(animationFrameId);
if (gl) {
gl.deleteProgram(program);
gl.deleteShader(vert);
gl.deleteShader(frag);
gl.deleteBuffer(buffer);
}
};
}, [
activePrimary,
activeSecondary,
activeTertiary,
activeHighlight,
ditherPattern,
speed,
scale,
intensity,
ditherScale,
grainAmount,
contrast,
rotationAngle,
activeVignette,
opacity,
interactive,
]);
const handleMouseMove = (e: React.MouseEvent) => {
if (!interactive || !canvasRef.current) return;
const rect = canvasRef.current.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width;
const y = 1.0 - (e.clientY - rect.top) / rect.height;
mouseRef.current.targetX = Math.max(0, Math.min(1, x));
mouseRef.current.targetY = Math.max(0, Math.min(1, y));
};
const handleMouseLeave = () => {
if (!interactive) return;
mouseRef.current.targetX = 0.5;
mouseRef.current.targetY = 0.5;
};
return (
);
}
export default AnimatedGrain;
```
## Usage
```tsx
import { AnimatedGrain } from "@/components/lightswind/animated-grain"
```
```tsx
import { AnimatedGrain } from "@/components/lightswind/animated-grain";
export function Demo() {
return (
);
}
```