{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "cloud-background", "type": "registry:ui", "title": "Cloud Background", "description": "A flowing cloud background with WebGL shaders and theme support.", "dependencies": [ "ogl" ], "files": [ { "path": "registry/ruixenui/cloud-background.tsx", "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport { Renderer, Program, Mesh, Triangle, Vec2 } from \"ogl\";\nimport { cn } from \"@/lib/utils\";\n\nconst vertex = `\nattribute vec2 position;\nvoid main() {\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\n// Flowing cloud waves + layered noise + grain\nconst fragment = `\n#ifdef GL_ES\nprecision highp float;\n#endif\n\nuniform vec2 uResolution;\nuniform float uTime;\nuniform float uTheme; // 0.0 = light, 1.0 = dark\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 vec2 u = f * f * (3.0 - 2.0 * f);\n\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\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfloat fbm(vec2 p) {\n float v = 0.0;\n float a = 0.5;\n for (int i = 0; i < 5; i++) {\n v += a * noise(p);\n p *= 2.0;\n a *= 0.5;\n }\n return v;\n}\n\nmat2 rot(float a) {\n float s = sin(a), c = cos(a);\n return mat2(c, -s, s, c);\n}\n\nvoid main() {\n vec2 st = gl_FragCoord.xy / uResolution.xy;\n vec2 uv = (st - 0.5) * 2.0;\n uv.x *= uResolution.x / uResolution.y;\n\n float t = uTime * 0.25;\n\n uv *= rot(0.08 * sin(t * 1.7));\n\n vec2 p = uv * 1.2;\n\n // layered noise for cloud shapes\n float n = fbm(p + vec2(t * 0.8, -t * 0.55));\n float m = fbm(p * 1.8 - vec2(t * 0.35, t * 0.7));\n float n2 = fbm(p * 0.6 + vec2(-t * 0.3, t * 0.4) + n * 0.5);\n\n float w = 0.0;\n w += sin(p.x * 2.2 + t * 2.4 + n * 2.0);\n w += cos(p.y * 1.7 - t * 1.8 + m * 2.2);\n w *= 0.5;\n\n float bands = 0.5 + 0.5 * sin(w * 2.4 + n * 3.2);\n float glow = smoothstep(0.15, 0.85, bands);\n\n // secondary glow from the extra noise layer — fills more of the canvas\n float glow2 = smoothstep(0.3, 0.8, n2);\n glow = max(glow, glow2 * 0.6);\n\n vec3 lightA = vec3(0.94, 0.97, 1.00);\n vec3 lightB = vec3(0.16, 0.46, 0.95);\n\n vec3 darkA = vec3(0.06, 0.09, 0.16);\n vec3 darkB = vec3(0.10, 0.72, 0.96);\n\n vec3 baseA = mix(lightA, darkA, uTheme);\n vec3 baseB = mix(lightB, darkB, uTheme);\n\n vec3 col = mix(baseA, baseB, glow);\n\n // soft highlight\n col += 0.10 * vec3(0.8, 0.9, 1.0) * glow * (0.4 + 0.6 * sin(uTime * 0.6));\n\n // subtle grain\n float g = (hash(gl_FragCoord.xy + uTime * 60.0) - 0.5) * 0.025;\n col += g;\n\n gl_FragColor = vec4(col, 1.0);\n}\n`;\n\ntype Props = {\n darkTheme?: boolean;\n resolutionScale?: number; // 0.5..2.0 recommended\n className?: string;\n};\n\nexport default function CloudBackground({\n darkTheme = false,\n resolutionScale = 1.0,\n className,\n}: Props) {\n const canvasRef = useRef(null);\n\n const rendererRef = useRef(null);\n const programRef = useRef(null);\n const rafRef = useRef(null);\n const roRef = useRef(null);\n const startRef = useRef(0);\n\n // Initialize once\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const parent = canvas.parentElement;\n if (!parent) return;\n\n const baseDpr = Math.min(window.devicePixelRatio || 1, 2);\n const clampedScale = Math.min(Math.max(resolutionScale, 0.5), 2);\n\n const renderer = new Renderer({\n canvas,\n alpha: true,\n antialias: true,\n dpr: baseDpr * clampedScale,\n });\n\n rendererRef.current = renderer;\n\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, 0);\n\n const geometry = new Triangle(gl);\n\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n uTime: { value: 0 },\n uResolution: { value: new Vec2(1, 1) },\n uTheme: { value: darkTheme ? 1 : 0 },\n },\n });\n\n programRef.current = program;\n\n const mesh = new Mesh(gl, { geometry, program });\n\n const resize = () => {\n const w = parent.clientWidth;\n const h = parent.clientHeight;\n\n // If parent has no height, nothing can render.\n if (!w || !h) return;\n\n renderer.setSize(w, h);\n\n // Match gl_FragCoord scale exactly:\n program.uniforms.uResolution.value.set(\n gl.drawingBufferWidth,\n gl.drawingBufferHeight,\n );\n };\n\n roRef.current = new ResizeObserver(resize);\n roRef.current.observe(parent);\n\n // First resize after layout\n resize();\n\n startRef.current = performance.now();\n\n const loop = () => {\n const now = performance.now();\n program.uniforms.uTime.value = (now - startRef.current) / 1000;\n\n renderer.render({ scene: mesh });\n rafRef.current = requestAnimationFrame(loop);\n };\n\n rafRef.current = requestAnimationFrame(loop);\n\n return () => {\n if (rafRef.current) cancelAnimationFrame(rafRef.current);\n roRef.current?.disconnect();\n roRef.current = null;\n\n rendererRef.current = null;\n programRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Update theme without recreating WebGL\n useEffect(() => {\n const program = programRef.current;\n if (!program) return;\n program.uniforms.uTheme.value = darkTheme ? 1 : 0;\n }, [darkTheme]);\n\n // Update render quality without recreating WebGL\n useEffect(() => {\n const renderer = rendererRef.current;\n const program = programRef.current;\n const canvas = canvasRef.current;\n if (!renderer || !program || !canvas) return;\n\n const parent = canvas.parentElement;\n if (!parent) return;\n\n const baseDpr = Math.min(window.devicePixelRatio || 1, 2);\n const clampedScale = Math.min(Math.max(resolutionScale, 0.5), 2);\n\n renderer.dpr = baseDpr * clampedScale;\n\n const w = parent.clientWidth;\n const h = parent.clientHeight;\n if (!w || !h) return;\n\n renderer.setSize(w, h);\n const gl = renderer.gl;\n program.uniforms.uResolution.value.set(\n gl.drawingBufferWidth,\n gl.drawingBufferHeight,\n );\n }, [resolutionScale]);\n\n return (\n \n );\n}\n", "type": "registry:ui", "target": "components/ruixen/cloud-background.tsx" } ] }