shadimage
Forgeui
uicosmicriftsource: ok · hit

Cosmic Rift

A WebGL-powered animated cosmic background featuring a procedural starfield with twinkling effects and flowing nebula-like wave distortions, rendered in real time using shaders.

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

No selection tags yet — empty means no evidence, not lack of support.

Files1 · source cached 10/9/2026, 5:39:57 AM

registry/components/cosmicrift/cosmicrift.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_colorBackground;
uniform vec3 u_colorStars;
uniform float u_density;
uniform float u_speed;

float hash(vec2 p) {
  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
}

void main() {
  vec2 uv = gl_FragCoord.xy / u_resolution;
  float aspect = u_resolution.x / u_resolution.y;
  
  vec2 p = uv;
  p.x *= aspect;

  float t = u_time * u_speed * 0.5;

  vec3 color = u_colorBackground;

  float gridSize = 35.0 * u_density; 
  vec2 gridPos = floor(p * gridSize);
  vec2 localUV = fract(p * gridSize) - 0.5;
  
  float r = hash(gridPos);
  float threshold = 0.85; // Low threshold = dense stars
  
  if (r > threshold) {
    vec2 offset = vec2(hash(gridPos + 12.0), hash(gridPos + 24.0)) - 0.5;
    float dist = length(localUV - offset * 0.7);
    
    float size = 0.025 + (r - threshold) * 0.6;
    float brightness = smoothstep(size, 0.0, dist);
    
    float twinkleSpeed = 1.0 + hash(gridPos + 100.0) * 3.0;
    float twinkle = sin(t * twinkleSpeed * 2.0 + hash(gridPos + 200.0) * 6.28) * 0.5 + 0.5;
    brightness *= (0.3 + twinkle * 0.7); 
    
    vec3 starCol = mix(u_colorStars, vec3(0.6, 0.8, 1.0), hash(gridPos + 300.0));
    
    color += starCol * brightness * (r - threshold) * 7.0;
  }

  vec2 cuv = uv * 2.0 - 1.0;
  cuv.x *= aspect;
  
  vec3 waveColor = vec3(0.0);
  
  for(float i = 1.0; i <= 4.0; i++) {
      float xDistort = sin(cuv.y * 2.0 * i + t * (0.5 + i * 0.15)) * 0.2;
      float yDistort = cos(cuv.x * 2.0 * i - t * 0.4) * 0.15;
      
      vec2 distortedUV = cuv + vec2(xDistort, yDistort);
      float dist = abs(distortedUV.y);
      
      float glow = 0.015 / (dist + 0.015);
      
      vec3 c1 = vec3(0.1, 0.02, 0.3); 
      vec3 c2 = vec3(0.0, 0.9, 1.0);  
      vec3 c3 = vec3(0.9, 0.1, 0.7); 
      
      vec3 layerCol = mix(c1, c2, sin(cuv.x * 2.5 + t + i) * 0.5 + 0.5);
      layerCol = mix(layerCol, c3, cos(cuv.y * 3.0 - t) * 0.5 + 0.5);
      
      waveColor += layerCol * glow * (1.5 / i);
  }
  
  float mask = smoothstep(1.0, 0.05, abs(cuv.y));
  
  color += waveColor * mask;

  float vignette = uv.x * (1.0 - uv.x) * uv.y * (1.0 - uv.y) * 15.0;
  color *= pow(vignette, 0.15);

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

interface CosmicRiftProps extends React.HTMLAttributes<HTMLDivElement> {
  colorBackground?: string;
  colorStars?: string;
  density?: number;
  speed?: number;
  height?: string;
}

const DEFAULT_COLOR_DARK = "#020106";
const DEFAULT_COLOR_STARS = "#ffffff";

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_DARK).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 CosmicRift = ({
  colorBackground = DEFAULT_COLOR_DARK,
  colorStars = DEFAULT_COLOR_STARS,
  density = 1.2,
  speed = 1,
  height = "100vh",
  className,
  style,
  ...props
}: CosmicRiftProps) => {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const hostRef = useRef<HTMLDivElement | null>(null);

  const settings = useMemo(
    () => ({
      colorBackground,
      colorStars,
      density,
      speed,
    }),
    [colorBackground, colorStars, density, 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) return;

    const compileShader = (type: number, source: string) => {
      const shader = gl.createShader(type)!;
      gl.shaderSource(shader, source);
      gl.compileShader(shader);
      return shader;
    };

    const program = gl.createProgram()!;
    gl.attachShader(program, compileShader(gl.VERTEX_SHADER, vertexShaderGLSL));
    gl.attachShader(
      program,
      compileShader(gl.FRAGMENT_SHADER, fragmentShaderGLSL),
    );
    gl.linkProgram(program);
    gl.useProgram(program);

    const vertices = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);
    const buffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, vertices, 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"),
      bg: gl.getUniformLocation(program, "u_colorBackground"),
      stars: gl.getUniformLocation(program, "u_colorStars"),
      density: gl.getUniformLocation(program, "u_density"),
      speed: gl.getUniformLocation(program, "u_speed"),
    };

    const resize = () => {
      const dpr = window.devicePixelRatio || 1;
      const rect = host.getBoundingClientRect();
      canvas.width = rect.width * dpr;
      canvas.height = rect.height * dpr;
      gl.viewport(0, 0, canvas.width, canvas.height);
    };

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

    let frameId: number;
    const render = (time: number) => {
      const bgRGB = hexToRgbNormalized(settings.colorBackground);
      const starRGB = hexToRgbNormalized(settings.colorStars);

      gl.uniform2f(locs.res, canvas.width, canvas.height);
      gl.uniform1f(locs.time, time * 0.001);
      gl.uniform3f(locs.bg, bgRGB[0], bgRGB[1], bgRGB[2]);
      gl.uniform3f(locs.stars, starRGB[0], starRGB[1], starRGB[2]);
      gl.uniform1f(locs.density, settings.density);
      gl.uniform1f(locs.speed, settings.speed);

      gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
      frameId = requestAnimationFrame(render);
    };

    frameId = requestAnimationFrame(render);

    return () => {
      cancelAnimationFrame(frameId);
      observer.disconnect();
      gl.deleteProgram(program);
    };
  }, [settings]);

  return (
    <div
      ref={hostRef}
      className={cn(
        "relative flex w-full items-center overflow-hidden bg-[#020106]",
        className,
      )}
      style={{ height, ...style }}
      {...props}
    >
      <canvas
        ref={canvasRef}
        className="pointer-events-none absolute inset-0 block h-full w-full"
      />
    </div>
  );
};

export default CosmicRift;

More from Forgeui

Browse all components in this library →