{ "name": "webgl-liquid", "type": "registry:ui", "dependencies": [ "clsx", "tailwind-merge" ], "registryDependencies": [], "description": "Premium WebGL liquid hero background with customizable flow, palette, reveal, and grain.", "files": [ { "path": "components/ui/webgl-liquid.tsx", "content": "\"use client\";\n\nimport { useEffect, useMemo, useRef } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst VERTEX_SHADER = `\nattribute vec2 position;\nvoid main() {\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst FRAGMENT_SHADER = `\nprecision highp float;\n\nuniform vec2 u_res;\nuniform float u_time;\nuniform vec3 u_colorDeep;\nuniform vec3 u_colorMid;\nuniform vec3 u_colorHighlight;\nuniform float u_speed;\nuniform float u_flowStrength;\nuniform float u_grain;\nuniform float u_contrast;\nuniform float u_opacity;\nuniform float u_reveal;\n\nfloat hash(vec2 p) {\n return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);\n}\n\nfloat noise(vec2 p) {\n vec2 i = floor(p);\n vec2 f = fract(p);\n float a = hash(i);\n float b = hash(i + vec2(1.0, 0.0));\n float c = hash(i + vec2(0.0, 1.0));\n float d = hash(i + vec2(1.0, 1.0));\n vec2 u = f * f * (3.0 - 2.0 * f);\n return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;\n}\n\nfloat fbm(vec2 p) {\n float v = 0.0;\n float a = 0.5;\n mat2 rot = mat2(0.86, 0.51, -0.51, 0.86);\n for (int i = 0; i < 6; i++) {\n v += a * noise(p);\n p = rot * p * 2.0;\n a *= 0.5;\n }\n return v;\n}\n\nvec3 applyContrast(vec3 c, float contrast) {\n return clamp((c - 0.5) * contrast + 0.5, 0.0, 1.0);\n}\n\nvoid main() {\n vec2 uv = gl_FragCoord.xy / u_res;\n float t = u_time * (0.14 * u_speed);\n vec2 aspect = vec2(u_res.x / max(u_res.y, 1.0), 1.0);\n vec2 p = (uv - 0.5) * aspect;\n\n vec2 flowP = vec2(p.x * 1.1, p.y - t * 0.35);\n float n1 = fbm(flowP * 2.8 + vec2(0.0, t * 0.2));\n float n2 = fbm((flowP + n1 * 0.45) * 4.0 - vec2(0.0, t * 0.35));\n float n3 = fbm((flowP + n2 * 0.4) * 6.5 + vec2(t * 0.15, 0.0));\n\n float structure = n3 * 1.15 + (n2 - 0.5) * 0.5;\n structure += (n1 - 0.5) * 0.3 * u_flowStrength;\n\n float lowBand = smoothstep(0.18, 0.6, structure);\n float highBand = smoothstep(0.62, 1.08, structure);\n vec3 col = mix(u_colorDeep, u_colorMid, lowBand);\n col = mix(col, u_colorHighlight, highBand);\n\n float glow = smoothstep(0.52, 0.95, structure) * (0.35 + 0.5 * u_flowStrength);\n col += glow * u_colorHighlight * 0.35;\n\n float verticalMask = smoothstep(1.05, 0.05, uv.y);\n verticalMask = pow(verticalMask, 1.1);\n\n float vignette = smoothstep(1.28, 0.36, length(uv - 0.5));\n col *= mix(0.9, 1.05, vignette);\n\n col = applyContrast(col, u_contrast);\n\n float dither = (hash(gl_FragCoord.xy + t * 10.0) - 0.5) * u_grain;\n col += dither;\n\n float alpha = verticalMask * smoothstep(0.08, 0.95, structure);\n alpha *= smoothstep(0.0, 0.28, u_reveal - uv.x);\n alpha *= u_opacity;\n\n gl_FragColor = vec4(clamp(col, 0.0, 1.0), clamp(alpha, 0.0, 1.0));\n}\n`;\n\nconst HEX_COLOR_REGEX = /^#?[0-9a-fA-F]{6}$/;\nconst FALLBACK_DEEP = \"#04050b\";\nconst FALLBACK_MID = \"#134d93\";\nconst FALLBACK_HIGHLIGHT = \"#8cecff\";\n\nfunction sanitizeHexColor(value: string, fallback: string) {\n const trimmed = value.trim();\n if (!HEX_COLOR_REGEX.test(trimmed)) {\n return fallback;\n }\n return trimmed.startsWith(\"#\") ? trimmed : `#${trimmed}`;\n}\n\nfunction hexToRgb01(hex: string): [number, number, number] {\n const normalized = sanitizeHexColor(hex, FALLBACK_DEEP).replace(\"#\", \"\");\n const r = parseInt(normalized.slice(0, 2), 16) / 255;\n const g = parseInt(normalized.slice(2, 4), 16) / 255;\n const b = parseInt(normalized.slice(4, 6), 16) / 255;\n return [r, g, b];\n}\n\nexport interface WebGLLiquidProps extends React.HTMLAttributes {\n title?: string;\n subtitle?: string;\n description?: string;\n colorDeep?: string;\n colorMid?: string;\n colorHighlight?: string;\n speed?: number;\n flowStrength?: number;\n grain?: number;\n contrast?: number;\n opacity?: number;\n reveal?: boolean;\n delayMs?: number;\n revealDuration?: number;\n children?: React.ReactNode;\n}\n\nconst LIQUID_HEADLINE_CLASS =\n \"pb-[0.08em] text-[11cqi] md:text-[7cqi] lg:text-[5.5cqi] leading-[0.92] tracking-[-0.03em] font-semibold text-white\";\n\nexport function WebGLLiquid({\n title = \"Fluid Motion\",\n subtitle = \"Premium Presence\",\n description = \"A cinematic liquid field tuned for modern hero sections with polished depth and restrained motion.\",\n colorDeep = FALLBACK_DEEP,\n colorMid = FALLBACK_MID,\n colorHighlight = FALLBACK_HIGHLIGHT,\n speed = 1,\n flowStrength = 1,\n grain = 0.05,\n contrast = 1.1,\n opacity = 0.95,\n reveal = true,\n delayMs = 0,\n revealDuration = 1.2,\n className,\n children,\n style,\n ...props\n}: WebGLLiquidProps) {\n const canvasRef = useRef(null);\n const hostRef = useRef(null);\n\n const settings = useMemo(\n () => ({\n colorDeep,\n colorMid,\n colorHighlight,\n speed,\n flowStrength,\n grain,\n contrast,\n opacity,\n reveal,\n delayMs,\n revealDuration,\n }),\n [\n colorDeep,\n colorMid,\n colorHighlight,\n speed,\n flowStrength,\n grain,\n contrast,\n opacity,\n reveal,\n delayMs,\n revealDuration,\n ],\n );\n\n useEffect(() => {\n const canvas = canvasRef.current;\n const host = hostRef.current;\n if (!canvas || !host) {\n return;\n }\n\n const gl = canvas.getContext(\"webgl\", { antialias: true, alpha: true });\n if (!gl) {\n return;\n }\n\n const compileShader = (type: number, source: string) => {\n const shader = gl.createShader(type);\n if (!shader) {\n return null;\n }\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n gl.deleteShader(shader);\n return null;\n }\n return shader;\n };\n\n const vertexShader = compileShader(gl.VERTEX_SHADER, VERTEX_SHADER);\n const fragmentShader = compileShader(gl.FRAGMENT_SHADER, FRAGMENT_SHADER);\n if (!vertexShader || !fragmentShader) {\n return;\n }\n\n const program = gl.createProgram();\n if (!program) {\n gl.deleteShader(vertexShader);\n gl.deleteShader(fragmentShader);\n return;\n }\n\n gl.attachShader(program, vertexShader);\n gl.attachShader(program, fragmentShader);\n gl.linkProgram(program);\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n gl.deleteProgram(program);\n gl.deleteShader(vertexShader);\n gl.deleteShader(fragmentShader);\n return;\n }\n\n gl.useProgram(program);\n\n const positionLocation = gl.getAttribLocation(program, \"position\");\n const quadBuffer = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);\n gl.bufferData(\n gl.ARRAY_BUFFER,\n new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),\n gl.STATIC_DRAW,\n );\n gl.enableVertexAttribArray(positionLocation);\n gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);\n\n const uRes = gl.getUniformLocation(program, \"u_res\");\n const uTime = gl.getUniformLocation(program, \"u_time\");\n const uColorDeep = gl.getUniformLocation(program, \"u_colorDeep\");\n const uColorMid = gl.getUniformLocation(program, \"u_colorMid\");\n const uColorHighlight = gl.getUniformLocation(program, \"u_colorHighlight\");\n const uSpeed = gl.getUniformLocation(program, \"u_speed\");\n const uFlowStrength = gl.getUniformLocation(program, \"u_flowStrength\");\n const uGrain = gl.getUniformLocation(program, \"u_grain\");\n const uContrast = gl.getUniformLocation(program, \"u_contrast\");\n const uOpacity = gl.getUniformLocation(program, \"u_opacity\");\n const uReveal = gl.getUniformLocation(program, \"u_reveal\");\n\n if (\n !uRes ||\n !uTime ||\n !uColorDeep ||\n !uColorMid ||\n !uColorHighlight ||\n !uSpeed ||\n !uFlowStrength ||\n !uGrain ||\n !uContrast ||\n !uOpacity ||\n !uReveal\n ) {\n gl.deleteBuffer(quadBuffer);\n gl.deleteProgram(program);\n gl.deleteShader(vertexShader);\n gl.deleteShader(fragmentShader);\n return;\n }\n\n const resize = () => {\n const dpr = Math.min(window.devicePixelRatio || 1, 2);\n const { width, height } = host.getBoundingClientRect();\n canvas.width = Math.max(1, Math.floor(width * dpr));\n canvas.height = Math.max(1, Math.floor(height * dpr));\n gl.viewport(0, 0, canvas.width, canvas.height);\n gl.uniform2f(uRes, canvas.width, canvas.height);\n };\n\n resize();\n const resizeObserver = new ResizeObserver(resize);\n resizeObserver.observe(host);\n\n let rafId = 0;\n const start = performance.now();\n\n const render = (now: number) => {\n const elapsedSec = Math.max(0, (now - start - settings.delayMs) / 1000);\n const revealProgress = settings.reveal\n ? Math.min(1, elapsedSec / Math.max(settings.revealDuration, 0.05))\n : 1;\n\n const deep = hexToRgb01(settings.colorDeep);\n const mid = hexToRgb01(settings.colorMid);\n const highlight = hexToRgb01(settings.colorHighlight);\n\n gl.clearColor(0, 0, 0, 0);\n gl.clear(gl.COLOR_BUFFER_BIT);\n\n gl.uniform1f(uTime, elapsedSec);\n gl.uniform3f(uColorDeep, deep[0], deep[1], deep[2]);\n gl.uniform3f(uColorMid, mid[0], mid[1], mid[2]);\n gl.uniform3f(uColorHighlight, highlight[0], highlight[1], highlight[2]);\n gl.uniform1f(uSpeed, settings.speed);\n gl.uniform1f(uFlowStrength, settings.flowStrength);\n gl.uniform1f(uGrain, settings.grain);\n gl.uniform1f(uContrast, settings.contrast);\n gl.uniform1f(uOpacity, settings.opacity);\n gl.uniform1f(uReveal, revealProgress);\n gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n\n rafId = requestAnimationFrame(render);\n };\n\n rafId = requestAnimationFrame(render);\n\n return () => {\n cancelAnimationFrame(rafId);\n resizeObserver.disconnect();\n gl.deleteBuffer(quadBuffer);\n gl.deleteProgram(program);\n gl.deleteShader(vertexShader);\n gl.deleteShader(fragmentShader);\n };\n }, [settings]);\n\n return (\n \n \n\n
\n
\n\n {(title || subtitle || description || children) && (\n
\n
\n {title &&

{title}

}\n {subtitle && (\n

\n {subtitle}\n

\n )}\n {description && (\n

\n {description}\n

\n )}\n {children &&
{children}
}\n
\n
\n )}\n
\n );\n}\n\nexport default WebGLLiquid;\n", "type": "registry:ui" } ] }