Forgeui
uichromatic-fluidsource: ok · refreshed
Chromatic Fluid
A WebGL-powered animated fluid effect featuring procedural noise, chromatic glint highlights, and dynamic lighting with film grain.
npx shadcn add "https://forgeui.in/r/chromatic-fluid.json"free
Visual style
particle
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Files1 · source cached 10/8/2026, 4:10:21 AM
registry/components/chromaticfluid/chromaticfluid.tsx
"use client";
import { useEffect, useMemo, useRef } from "react";
import { cn } from "@/lib/utils";
const vertexShaderGLSL = `
attribute vec2 position;
varying vec2 vUv;
void main() {
vUv = (position + 1.0) * 0.5;
gl_Position = vec4(position, 0.0, 1.0);
}
`;
const fragmentShaderGLSL = `
precision highp float;
uniform vec2 u_resolution;
uniform float u_time;
uniform float u_flowStrength;
uniform float u_grain;
uniform float u_contrast;
uniform float u_speed;
varying vec2 vUv;
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(mix(hash(i), hash(i + vec2(1.0, 0.0)), u.x),
mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x), u.y);
}
float fbm(vec2 p) {
float v = 0.0;
float a = 0.5;
mat2 rot = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.5));
for (int i = 0; i < 4; i++) {
v += a * noise(p);
p = rot * p * 2.0;
a *= 0.5;
}
return v;
}
void main() {
vec2 uv = vUv;
float t = u_time * u_speed;
vec2 p = (uv - 0.5) * vec2(u_resolution.x / u_resolution.y, 1.0) * 2.0;
float baseNoise = fbm(p * 0.8 + t * 0.2);
float fluid = fbm(p * 1.2 + baseNoise * u_flowStrength + t * 0.3);
float eps = 0.01;
float nx = fbm(p + vec2(eps, 0.0) + baseNoise * u_flowStrength + t * 0.3) - fluid;
float ny = fbm(p + vec2(0.0, eps) + baseNoise * u_flowStrength + t * 0.3) - fluid;
vec3 normal = normalize(vec3(nx, ny, eps * 1.5));
vec3 lightDir = normalize(vec3(1.0, 1.0, 0.8));
vec3 viewDir = vec3(0.0, 0.0, 1.0);
vec3 halfVector = normalize(lightDir + viewDir);
vec3 color = vec3(0.0);
float glintIntensity = 64.0;
float specR = pow(max(dot(normalize(vec3(nx + 0.005, ny, eps*1.5)), halfVector), 0.0), glintIntensity);
float specG = pow(max(dot(normal, halfVector), 0.0), glintIntensity);
float specB = pow(max(dot(normalize(vec3(nx - 0.005, ny, eps*1.5)), halfVector), 0.0), glintIntensity);
vec3 specular = vec3(specR, specG, specB) * 1.5;
color += specular;
color = mix(vec3(0.5), color, u_contrast);
float vig = smoothstep(1.8, 0.2, length(uv - 0.5));
color *= vig;
float grainAmount = (hash(uv + t) - 0.5) * u_grain;
color += grainAmount;
gl_FragColor = vec4(clamp(color, 0.0, 1.0), 1.0);
}
`;
interface ChromaticFluidProps extends React.HTMLAttributes<HTMLDivElement> {
flowStrength?: number;
grain?: number;
contrast?: number;
speed?: number;
height?: string;
}
const ChromaticFluid = ({
flowStrength = 0.8,
grain = 0.04,
contrast = 1.1,
speed = 0.4,
height = "100vh",
className,
style,
...props
}: ChromaticFluidProps) => {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const hostRef = useRef<HTMLDivElement | null>(null);
const settings = useMemo(
() => ({
flowStrength,
grain,
contrast,
speed,
}),
[flowStrength, grain, contrast, speed],
);
useEffect(() => {
const canvas = canvasRef.current;
const host = hostRef.current;
if (!canvas || !host) return;
const gl = canvas.getContext("webgl", { antialias: true });
if (!gl) return;
const compile = (type: number, src: string) => {
const s = gl.createShader(type)!;
gl.shaderSource(s, src);
gl.compileShader(s);
return s;
};
const program = gl.createProgram()!;
gl.attachShader(program, compile(gl.VERTEX_SHADER, vertexShaderGLSL));
gl.attachShader(program, compile(gl.FRAGMENT_SHADER, fragmentShaderGLSL));
gl.linkProgram(program);
gl.useProgram(program);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
gl.STATIC_DRAW,
);
const pos = gl.getAttribLocation(program, "position");
gl.enableVertexAttribArray(pos);
gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);
const locs = {
res: gl.getUniformLocation(program, "u_resolution"),
time: gl.getUniformLocation(program, "u_time"),
flow: gl.getUniformLocation(program, "u_flowStrength"),
grain: gl.getUniformLocation(program, "u_grain"),
contrast: gl.getUniformLocation(program, "u_contrast"),
speed: gl.getUniformLocation(program, "u_speed"),
};
const resize = () => {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const { width, height } = host.getBoundingClientRect();
canvas.width = Math.max(1, Math.floor(width * dpr));
canvas.height = Math.max(1, Math.floor(height * dpr));
gl.viewport(0, 0, canvas.width, canvas.height);
gl.uniform2f(locs.res, canvas.width, canvas.height);
};
resize();
const resizeObserver = new ResizeObserver(resize);
resizeObserver.observe(host);
let frameId: number;
const startTime = performance.now();
const render = (now: number) => {
const elapsed = (now - startTime) / 1000;
gl.uniform1f(locs.time, elapsed);
gl.uniform1f(locs.flow, settings.flowStrength);
gl.uniform1f(locs.grain, settings.grain);
gl.uniform1f(locs.contrast, settings.contrast);
gl.uniform1f(locs.speed, settings.speed);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
frameId = requestAnimationFrame(render);
};
frameId = requestAnimationFrame(render);
return () => {
cancelAnimationFrame(frameId);
resizeObserver.disconnect();
};
}, [settings]);
return (
<div
ref={hostRef}
className={cn(
"relative flex min-h-screen w-full items-center justify-center overflow-hidden bg-black",
className,
)}
style={{ height, ...style }}
{...props}
>
<canvas
ref={canvasRef}
className="pointer-events-none absolute inset-0 block h-full w-full"
/>
<div className="relative z-10 w-full">{props.children}</div>
</div>
);
};
export default ChromaticFluid;