shadimage
Forgeui
uicloudscapesource: ok · hit

Cloudscape

A WebGL-powered animated cloud gradient that creates a soft, flowing sky-like background using layered noise and color blending.

npx shadcn add "https://forgeui.in/r/cloudscape.json"
free

Visual style

gradientparticlepastel

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Files1 · source cached 10/9/2026, 11:10:25 PM

registry/components/cloudscape/cloudscape.tsx
"use client";

import { useEffect, useMemo, useRef } from "react";
import { cn } from "@/lib/utils";

const vertexShaderGLSL = `
attribute vec2 position;
void main() {
  gl_Position = vec4(position, 0.0, 1.0);
}
`;

const fragmentShaderGLSL = `
precision highp float;

uniform vec2 u_resolution;
uniform float u_time;
uniform vec3 u_colorBottom;
uniform vec3 u_colorMid;
uniform vec3 u_colorTop;
uniform float u_speed;

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);
  float a = hash(i);
  float b = hash(i + vec2(1.0, 0.0));
  float c = hash(i + vec2(0.0, 1.0));
  float d = hash(i + vec2(1.0, 1.0));
  vec2 u = f * f * (3.0 - 2.0 * f);
  return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
}

float fbm(vec2 p, float t) {
  float v = 0.0;
  float a = 0.5;
  float fi = 0.0;
  mat2 rot = mat2(0.86, 0.51, -0.51, 0.86);
  
  for (int i = 0; i < 6; i++) {
    vec2 morph = vec2(sin(t * 0.5 + fi), cos(t * 0.3 - fi)) * 0.05;
    v += a * noise(p + morph);
    p = rot * p * 2.0;
    a *= 0.5;
    fi += 1.0;
  }
  return v;
}

void main() {
  vec2 uv = gl_FragCoord.xy / u_resolution;
  float t = u_time * u_speed;
  vec2 aspect = vec2(u_resolution.x / max(u_resolution.y, 1.0), 1.0);
  vec2 p = (uv - 0.5) * aspect;

  vec2 wind = vec2(t * 0.1, t * 0.02);

  float pattern = fbm(p * 2.2 - wind, t);

  float bandLow = smoothstep(0.3, 0.65, pattern);
  float bandHigh = smoothstep(0.7, 0.95, pattern); 
  
  vec3 color = mix(u_colorBottom, u_colorMid, bandLow);
  color = mix(color, u_colorTop, bandHigh);

  gl_FragColor = vec4(color, 1.0);
}
`;

interface CloudscapeProps extends React.HTMLAttributes<HTMLDivElement> {
  colorBottom?: string;
  colorMid?: string;
  colorTop?: string;
  speed?: number;
  height?: string;
}

const DEFAULT_COLOR = "#0d1117";

const COLOR_HEX_PATTERN = /^#?[0-9a-fA-F]{6}$/;

function normalizeHexColor(value: string, fallback: string) {
  const trimmed = value.trim();
  if (!COLOR_HEX_PATTERN.test(trimmed)) {
    return fallback;
  }
  return trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
}

function hexToRgbNormalized(hex: string): [number, number, number] {
  const normalized = normalizeHexColor(hex, DEFAULT_COLOR).replace("#", "");
  const r = parseInt(normalized.slice(0, 2), 16) / 255;
  const g = parseInt(normalized.slice(2, 4), 16) / 255;
  const b = parseInt(normalized.slice(4, 6), 16) / 255;
  return [r, g, b];
}

const Cloudscape = ({
  colorBottom = "#87ceeb",
  colorMid = "#f8f8f8",
  colorTop = "#ffffff",
  speed = 1,
  height = "100vh",
  className,
  style,
  ...props
}: CloudscapeProps) => {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const hostRef = useRef<HTMLDivElement | null>(null);

  const settings = useMemo(
    () => ({
      colorBottom,
      colorMid,
      colorTop,
      speed,
    }),
    [colorBottom, colorMid, colorTop, speed],
  );

  useEffect(() => {
    const canvas = canvasRef.current;
    const host = hostRef.current;
    if (!canvas || !host) {
      return;
    }

    const gl = canvas.getContext("webgl", { antialias: true, alpha: true });
    if (!gl) {
      console.error("WebGL not supported");
      return;
    }

    const compileGLSLShader = (type: number, source: string) => {
      const shader = gl.createShader(type);
      if (!shader) {
        return null;
      }
      gl.shaderSource(shader, source);
      gl.compileShader(shader);
      if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
        console.error("Shader compile error:", gl.getShaderInfoLog(shader));
        gl.deleteShader(shader);
        return null;
      }
      return shader;
    };

    const vertexShaderObject = compileGLSLShader(
      gl.VERTEX_SHADER,
      vertexShaderGLSL,
    );
    const fragmentShaderObject = compileGLSLShader(
      gl.FRAGMENT_SHADER,
      fragmentShaderGLSL,
    );
    if (!vertexShaderObject || !fragmentShaderObject) {
      return;
    }

    const glProgram = gl.createProgram();
    if (!glProgram) {
      return;
    }

    gl.attachShader(glProgram, vertexShaderObject);
    gl.attachShader(glProgram, fragmentShaderObject);
    gl.linkProgram(glProgram);

    if (!gl.getProgramParameter(glProgram, gl.LINK_STATUS)) {
      console.error("Program link error:", gl.getProgramInfoLog(glProgram));
      gl.deleteProgram(glProgram);
      gl.deleteShader(vertexShaderObject);
      gl.deleteShader(fragmentShaderObject);
      return;
    }

    gl.useProgram(glProgram);

    const vertexPositionAttribLocation = gl.getAttribLocation(
      glProgram,
      "position",
    );
    const screenQuadVertexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, screenQuadVertexBuffer);
    gl.bufferData(
      gl.ARRAY_BUFFER,
      new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
      gl.STATIC_DRAW,
    );
    gl.enableVertexAttribArray(vertexPositionAttribLocation);
    gl.vertexAttribPointer(
      vertexPositionAttribLocation,
      2,
      gl.FLOAT,
      false,
      0,
      0,
    );

    const resolutionUniformLocation = gl.getUniformLocation(
      glProgram,
      "u_resolution",
    );
    const timeUniformLocation = gl.getUniformLocation(glProgram, "u_time");
    const colorBottomUniformLocation = gl.getUniformLocation(
      glProgram,
      "u_colorBottom",
    );
    const colorMidUniformLocation = gl.getUniformLocation(
      glProgram,
      "u_colorMid",
    );
    const colorTopUniformLocation = gl.getUniformLocation(
      glProgram,
      "u_colorTop",
    );
    const speedUniformLocation = gl.getUniformLocation(glProgram, "u_speed");

    if (
      !resolutionUniformLocation ||
      !timeUniformLocation ||
      !colorBottomUniformLocation ||
      !colorMidUniformLocation ||
      !colorTopUniformLocation ||
      !speedUniformLocation
    ) {
      gl.deleteBuffer(screenQuadVertexBuffer);
      gl.deleteProgram(glProgram);
      gl.deleteShader(vertexShaderObject);
      gl.deleteShader(fragmentShaderObject);
      return;
    }

    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(resolutionUniformLocation, canvas.width, canvas.height);
    };

    resize();
    const resizeObserver = new ResizeObserver(resize);
    resizeObserver.observe(host);

    let animationFrameId = 0;
    const start = performance.now();

    const render = (now: number) => {
      const elapsedSec = (now - start) / 1000;

      const colorBottom = hexToRgbNormalized(settings.colorBottom);
      const colorMid = hexToRgbNormalized(settings.colorMid);
      const colorTop = hexToRgbNormalized(settings.colorTop);

      gl.clearColor(0, 0, 0, 0);
      gl.clear(gl.COLOR_BUFFER_BIT);

      gl.uniform1f(timeUniformLocation, elapsedSec);
      gl.uniform3f(
        colorBottomUniformLocation,
        colorBottom[0],
        colorBottom[1],
        colorBottom[2],
      );
      gl.uniform3f(
        colorMidUniformLocation,
        colorMid[0],
        colorMid[1],
        colorMid[2],
      );
      gl.uniform3f(
        colorTopUniformLocation,
        colorTop[0],
        colorTop[1],
        colorTop[2],
      );
      gl.uniform1f(speedUniformLocation, settings.speed);

      gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

      animationFrameId = requestAnimationFrame(render);
    };

    animationFrameId = requestAnimationFrame(render);

    return () => {
      cancelAnimationFrame(animationFrameId);
      resizeObserver.disconnect();
      gl.deleteBuffer(screenQuadVertexBuffer);
      gl.deleteProgram(glProgram);
      gl.deleteShader(vertexShaderObject);
      gl.deleteShader(fragmentShaderObject);
    };
  }, [settings]);

  return (
    <div
      ref={hostRef}
      className={cn(
        "relative flex w-full items-center overflow-hidden bg-black",
        className,
      )}
      style={{ height, containerType: "size", ...style }}
      {...props}
    >
      <canvas
        ref={canvasRef}
        aria-hidden="true"
        className="pointer-events-none absolute inset-0 h-full w-full"
        style={{ width: "100%", height: "100%", display: "block" }}
      />
    </div>
  );
};

export default Cloudscape;

More from Forgeui

Browse all components in this library →