SlicedWaves
A grid of soft glowing bars rippling like a slatted equalizer.
Live preview
Loading demo…
Installation
npx shadcn@latest add https://reactbits.dev/r/SlicedWaves-TS-TW.json
Dependencies: ogl
Source
'use client';
import { useEffect, useRef } from 'react';
import { Renderer, Program, Mesh, Triangle } from 'ogl';
export type SlicedWavesOrientation = 'horizontal' | 'vertical';
export interface SlicedWavesProps {
color1?: string;
color2?: string;
color3?: string;
columns?: number;
rows?: number;
barThickness?: number;
speed?: number;
travel?: number;
waveSpread?: number;
rowOffset?: number;
softness?: number;
glow?: number;
brightness?: number;
contrast?: number;
opacity?: number;
orientation?: SlicedWavesOrientation;
alternate?: boolean;
mouseInteraction?: boolean;
mouseStrength?: number;
mouseRadius?: number;
grain?: boolean;
grainIntensity?: number;
lightMode?: boolean;
className?: string;
}
const hexToRgb = (hex: string): [number, number, number] => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (!result) return [1, 1, 1];
return [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255];
};
const vertex = `#version 300 es
in vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
`;
const fragment = `#version 300 es
precision highp float;
uniform vec2 iResolution;
uniform float iTime;
uniform float uColumns;
uniform float uRows;
uniform float uThickness;
uniform float uSpeed;
uniform float uTravel;
uniform float uWaveSpread;
uniform float uRowOffset;
uniform float uSoftness;
uniform float uGlow;
uniform float uBrightness;
uniform float uContrast;
uniform float uOpacity;
uniform float uVertical;
uniform float uAlternate;
uniform vec2 uMouse;
uniform float uMouseStrength;
uniform float uMouseRadius;
uniform float uEnableMouse;
uniform float uMouseActive;
uniform float uGrain;
uniform float uGrainIntensity;
uniform float uLightMode;
uniform vec3 uColor1;
uniform vec3 uColor2;
uniform vec3 uColor3;
out vec4 fragColor;
void main() {
vec2 uv = gl_FragCoord.xy / iResolution.xy;
vec2 grid = vec2(max(uColumns, 1.0), max(uRows, 1.0));
vec2 p = uv * grid;
vec2 gv = fract(p) - 0.5;
vec2 id = floor(p);
float barCoord, waveId, offId, along;
if (uVertical > 0.5) {
barCoord = gv.x; waveId = id.y; offId = id.x; along = uv.y;
} else {
barCoord = gv.y; waveId = id.x; offId = id.y; along = uv.x;
}
float dir = 1.0;
if (uAlternate > 0.5 && mod(offId, 2.0) >= 1.0) dir = -1.0;
float phase = iTime * uSpeed + waveId * uWaveSpread + cos(offId * uRowOffset);
float mv = sin(phase) * 0.5 + 0.5;
if (dir < 0.0) mv = 1.0 - mv;
float infl = 0.0;
if (uEnableMouse > 0.5) {
float md = distance(uv, uMouse);
infl = smoothstep(uMouseRadius, 0.0, md) * uMouseStrength * uMouseActive;
}
float thick = clamp(uThickness + infl * 0.25, 0.0, 1.0);
float startPos = (0.5 - thick * 0.5) * uTravel;
float endPos = (-0.5 + thick * 0.5) * uTravel;
float pos = mix(startPos, endPos, mv);
float aa = max(uSoftness, 0.0005);
float d = abs(barCoord + pos) - thick * 0.5;
float aaWidth = fwidth(uVertical > 0.5 ? p.x : p.y);
float edge = max(aa, aaWidth);
float mask = smoothstep(edge, -edge, d);
float glow = exp(-max(d, 0.0) * (7.0 / (uGlow + 0.001))) * clamp(uGlow, 0.0, 1.0);
float intensity = clamp(mask + glow * (1.0 - mask), 0.0, 1.0);
if (uGrain > 0.5) {
float g = fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233)) + iTime) * 43758.5453);
intensity = clamp(intensity + (g - 0.5) * uGrainIntensity, 0.0, 1.0);
}
float tint = mv;
vec3 grad = mix(uColor2, uColor1, tint);
grad = mix(grad, uColor3, clamp(along, 0.0, 1.0) * 0.45);
vec3 col = grad * uBrightness * (1.0 + infl * 0.6);
col = (col - 0.5) * uContrast + 0.5;
col = clamp(col, 0.0, 1.0);
float a = intensity * uOpacity;
if (uLightMode > 0.5) {
float peak = max(col.r, max(col.g, col.b));
vec3 chroma = pow(clamp(col / max(peak, 0.0001), 0.0, 1.0), vec3(1.16));
fragColor = vec4(mix(vec3(1.0), chroma, a * 0.94), 1.0);
} else {
fragColor = vec4(col * a, a);
}
}
`;
type SlicedWavesCtx = {
renderer: InstanceType<typeof Renderer>;
program: InstanceType<typeof Program>;
mesh: InstanceType<typeof Mesh>;
};
const ctxMap = new WeakMap<HTMLDivElement, SlicedWavesCtx>();
const SlicedWaves: React.FC<SlicedWavesProps> = ({
color1 = '#FF9FFC',
color2 = '#5227FF',
color3 = '#B497CF',
columns = 14,
rows = 8,
barThickness = 0.1,
speed = 0.35,
travel = 0.7,
waveSpread = 0.9,
rowOffset = 1.0,
softness = 0.05,
glow = 0,
brightness = 1.0,
contrast = 1.0,
opacity = 0.5,
orientation = 'horizontal',
alternate = false,
mouseInteraction = true,
mouseStrength = 1,
mouseRadius = 0.3,
grain = true,
grainIntensity = 0.05,
lightMode = false,
className = ''
}) => {
const containerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const renderer = new Renderer({
webgl: 2,
alpha: true,
premultipliedAlpha: true,
antialias: false,
dpr: Math.min(window.devicePixelRatio || 1, 2)
});
const gl = renderer.gl;
gl.clearColor(0, 0, 0, 0);
const canvas = gl.canvas as HTMLCanvasElement;
canvas.style.width = '100%';
canvas.style.height = '100%';
canvas.style.display = 'block';
container.appendChild(canvas);
const geometry = new Triangle(gl);
const program = new Program(gl, {
vertex,
fragment,
uniforms: {
iTime: { value: 0 },
iResolution: { value: new Float32Array([1, 1]) },
uColumns: { value: 14 },
uRows: { value: 8 },
uThickness: { value: 0.1 },
uSpeed: { value: 0.35 },
uTravel: { value: 0.7 },
uWaveSpread: { value: 0.9 },
uRowOffset: { value: 1.0 },
uSoftness: { value: 0.05 },
uGlow: { value: 0 },
uBrightness: { value: 1.0 },
uContrast: { value: 1.0 },
uOpacity: { value: 0.5 },
uVertical: { value: 0.0 },
uAlternate: { value: 0.0 },
uMouse: { value: new Float32Array([0.5, 0.5]) },
uMouseStrength: { value: 1 },
uMouseRadius: { value: 0.3 },
uEnableMouse: { value: 1.0 },
uMouseActive: { value: 0.0 },
uGrain: { value: 1.0 },
uGrainIntensity: { value: 0.05 },
uLightMode: { value: 0.0 },
uColor1: { value: new Float32Array([1, 1, 1]) },
uColor2: { value: new Float32Array([1, 1, 1]) },
uColor3: { value: new Float32Array([1, 1, 1]) }
}
});
const mesh = new Mesh(gl, { geometry, program });
ctxMap.set(container, { renderer, program, mesh });
const setSize = () => {
const rect = container.getBoundingClientRect();
const w = Math.max(1, Math.floor(rect.width));
const h = Math.max(1, Math.floor(rect.height));
renderer.setSize(w, h);
const res = program.uniforms.iResolution.value as Float32Array;
res[0] = gl.drawingBufferWidth;
res[1] = gl.drawingBufferHeight;
renderer.render({ scene: mesh });
};
const ro = new ResizeObserver(setSize);
ro.observe(container);
setSize();
let currentMouse: [number, number] = [0.5, 0.5];
let targetMouse: [number, number] = [0.5, 0.5];
let currentActive = 0;
let targetActive = 0;
const onMouseMove = (e: MouseEvent) => {
const rect = canvas.getBoundingClientRect();
targetMouse = [(e.clientX - rect.left) / rect.width, 1.0 - (e.clientY - rect.top) / rect.height];
targetActive = 1;
};
const onMouseLeave = () => {
targetActive = 0;
};
canvas.addEventListener('mousemove', onMouseMove);
canvas.addEventListener('mouseleave', onMouseLeave);
let raf = 0;
let isVisible = true;
let isPageVisible = !document.hidden;
const t0 = performance.now();
const loop = (t: number) => {
program.uniforms.iTime.value = (t - t0) * 0.001;
currentMouse[0] += 0.05 * (targetMouse[0] - currentMouse[0]);
currentMouse[1] += 0.05 * (targetMouse[1] - currentMouse[1]);
const m = program.uniforms.uMouse.value as Float32Array;
m[0] = currentMouse[0];
m[1] = currentMouse[1];
currentActive += 0.05 * (targetActive - currentActive);
program.uniforms.uMouseActive.value = currentActive;
renderer.render({ scene: mesh });
raf = requestAnimationFrame(loop);
};
const tryStart = () => {
if (isVisible && isPageVisible && raf === 0) raf = requestAnimationFrame(loop);
};
const tryStop = () => {
if (raf !== 0) {
cancelAnimationFrame(raf);
raf = 0;
}
};
const io = new IntersectionObserver(
([entry]) => {
isVisible = entry.isIntersecting;
isVisible ? tryStart() : tryStop();
},
{ threshold: 0 }
);
io.observe(container);
const onVisibility = () => {
isPageVisible = !document.hidden;
isPageVisible ? tryStart() : tryStop();
};
document.addEventListener('visibilitychange', onVisibility);
tryStart();
return () => {
tryStop();
ro.disconnect();
io.disconnect();
document.removeEventListener('visibilitychange', onVisibility);
canvas.removeEventListener('mousemove', onMouseMove);
canvas.removeEventListener('mouseleave', onMouseLeave);
ctxMap.delete(container);
try {
container.removeChild(canvas);
} catch {}
gl.getExtension('WEBGL_lose_context')?.loseContext();
};
}, []);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const ctx = ctxMap.get(container);
if (!ctx) return;
const u = ctx.program.uniforms;
u.uColumns.value = Math.max(1, Math.round(columns));
u.uRows.value = Math.max(1, Math.round(rows));
u.uThickness.value = barThickness;
u.uSpeed.value = speed;
u.uTravel.value = travel;
u.uWaveSpread.value = waveSpread;
u.uRowOffset.value = rowOffset;
u.uSoftness.value = softness;
u.uGlow.value = glow;
u.uBrightness.value = brightness;
u.uContrast.value = contrast;
u.uOpacity.value = opacity;
u.uVertical.value = orientation === 'vertical' ? 1.0 : 0.0;
u.uAlternate.value = alternate ? 1.0 : 0.0;
u.uMouseStrength.value = mouseStrength;
u.uMouseRadius.value = mouseRadius;
u.uEnableMouse.value = mouseInteraction ? 1.0 : 0.0;
u.uGrain.value = grain ? 1.0 : 0.0;
u.uGrainIntensity.value = grainIntensity;
u.uLightMode.value = lightMode ? 1.0 : 0.0;
const c1 = hexToRgb(color1);
const a1 = u.uColor1.value as Float32Array;
a1[0] = c1[0];
a1[1] = c1[1];
a1[2] = c1[2];
const c2 = hexToRgb(color2);
const a2 = u.uColor2.value as Float32Array;
a2[0] = c2[0];
a2[1] = c2[1];
a2[2] = c2[2];
const c3 = hexToRgb(color3);
const a3 = u.uColor3.value as Float32Array;
a3[0] = c3[0];
a3[1] = c3[1];
a3[2] = c3[2];
}, [
color1,
color2,
color3,
columns,
rows,
barThickness,
speed,
travel,
waveSpread,
rowOffset,
softness,
glow,
brightness,
contrast,
opacity,
orientation,
alternate,
mouseInteraction,
mouseStrength,
mouseRadius,
grain,
grainIntensity,
lightMode
]);
return <div ref={containerRef} className={`relative h-full w-full overflow-hidden ${className}`.trim()} />;
};
export default SlicedWaves;