--- title: Wavy Ripple Background description: An interactive WebGL shader background featuring concentric waving ripple rings that expand and fade. Responds smoothly to mouse movements and supports full theme customizability. --- ## Installation CLI Manual ```bash npx lightswind@latest add wavy-ripple-background ``` Copy and paste the following code into your project. ```tsx "use client"; import { useEffect, useRef } from "react"; import { Renderer, Program, Mesh, Color, Triangle } from "ogl"; interface WavyRippleBackgroundProps { className?: string; /** Primary wave color in CSS format (hex, rgb, etc.) */ waveColor?: string; /** Background color of the container */ backgroundColor?: string; /** Animation speed multiplier (default: 1.2) */ speed?: number; /** Wave ripple frequency (default: 3.5) */ frequency?: number; /** Thickness scaling/sharpness of the rings (default: 3.5) */ ringSharpness?: number; /** Maximum opacity of the waves (default: 0.45) */ maxOpacity?: number; } const VERTEX_SHADER = `#version 300 es in vec2 position; void main() { gl_Position = vec4(position, 0.0, 1.0); }`; const FRAGMENT_SHADER = `#version 300 es precision highp float; uniform float uTime; uniform vec2 uResolution; uniform vec3 uWaveColor; uniform float uSpeed; uniform float uFrequency; uniform float uScale; uniform float uMaxOpacity; out vec4 fragColor; void main() { vec2 uv = gl_FragCoord.xy / uResolution; // Center is locked to bottom-middle vec2 center = vec2(0.5, 0.0); // Apply aspect ratio correction for perfect concentric circular arcs float aspect = uResolution.x / uResolution.y; vec2 dir = uv - center; dir.x *= aspect; float dist = length(dir); // Concentric wave angle float angle = dist * uFrequency - uTime * uSpeed; float wave = sin(angle); float cosWave = cos(angle); // Map sin wave from [-1, 1] to [0, 1] float normWave = wave * 0.5 + 0.5; // Shape the wave to be thick and pillowy (wide peaks, thin valleys) float ringIntensity = pow(normWave, uScale); // High-fidelity 3D shading: // Compute height derivative (slope) float dH = uFrequency * cosWave * uScale * pow(normWave, max(0.01, uScale - 1.0)) * 0.5; // Radial normal mapping vec2 normal2D = normalize(dir) * dH; vec3 normal = normalize(vec3(normal2D.x, normal2D.y, 1.0)); // Light from top-front vec3 lightDir = normalize(vec3(0.0, 1.0, 0.8)); // Diffuse illumination float diffuse = max(0.0, dot(normal, lightDir)); // Specular highlight for a glossy, premium plastic/glass finish vec3 viewDir = vec3(0.0, 0.0, 1.0); vec3 halfDir = normalize(lightDir + viewDir); float specular = pow(max(0.0, dot(normal, halfDir)), 24.0) * 0.3; // Combine wave color with 3D highlights and shadows vec3 litColor = uWaveColor * (0.35 + 0.65 * diffuse) + vec3(1.0) * specular; // Concentric half-ripple fade: // Fade out as it expands higher up the screen (uv.y: 0.0 -> 1.0) and further radially float distFade = smoothstep(0.85, 0.15, dist); float heightFade = smoothstep(0.85, 0.15, uv.y); float alpha = ringIntensity * distFade * heightFade * uMaxOpacity; fragColor = vec4(litColor, alpha); }`; export default function WavyRippleBackground({ className = "", waveColor = "#3b82f6", backgroundColor = "transparent", speed = 0.8, frequency = 3.5, // adjusted to show exactly 4 concentric dome levels ringSharpness = 0.55, // lowered to make waves thick and cylindrical maxOpacity = 0.65, }: WavyRippleBackgroundProps) { const containerRef = useRef(null); useEffect(() => { const container = containerRef.current; if (!container) return; // Create OGL context const renderer = new Renderer({ alpha: true, antialias: true }); const gl = renderer.gl; gl.clearColor(0, 0, 0, 0); gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); // Dynamic full viewport geometry const geometry = new Triangle(gl); if (geometry.attributes.uv) delete geometry.attributes.uv; // Create shader program const program = new Program(gl, { vertex: VERTEX_SHADER, fragment: FRAGMENT_SHADER, uniforms: { uTime: { value: 0 }, uResolution: { value: [container.offsetWidth, container.offsetHeight] }, uWaveColor: { value: (() => { const c = new Color(waveColor); return [c.r, c.g, c.b]; })(), }, uSpeed: { value: speed }, uFrequency: { value: frequency }, uScale: { value: ringSharpness }, uMaxOpacity: { value: maxOpacity }, }, }); const mesh = new Mesh(gl, { geometry, program }); container.appendChild(gl.canvas); // Handle resizing const resize = () => { const w = container.offsetWidth; const h = container.offsetHeight; renderer.setSize(w, h); program.uniforms.uResolution.value = [w, h]; }; window.addEventListener("resize", resize); resize(); let animationId: number; const animate = (time: number) => { animationId = requestAnimationFrame(animate); program.uniforms.uTime.value = time * 0.001; program.uniforms.uWaveColor.value = (() => { const c = new Color(waveColor); return [c.r, c.g, c.b]; })(); program.uniforms.uSpeed.value = speed; program.uniforms.uFrequency.value = frequency; program.uniforms.uScale.value = ringSharpness; program.uniforms.uMaxOpacity.value = maxOpacity; renderer.render({ scene: mesh }); }; animate(0); return () => { cancelAnimationFrame(animationId); window.removeEventListener("resize", resize); if (gl.canvas.parentNode === container) { container.removeChild(gl.canvas); } gl.getExtension("WEBGL_lose_context")?.loseContext(); }; }, [waveColor, speed, frequency, ringSharpness, maxOpacity]); return (
); } ``` ## Usage ```tsx import WavyRippleBackground from "@/components/lightswind/wavy-ripple-background"; ``` ```tsx import WavyRippleBackground from "@/components/lightswind/wavy-ripple-background"; export function Demo() { return (
); } ```