Background (Tailwind)

PreviousNext

A background component with various visual effects.

Docs
roiuiitem

Preview

Loading preview…
registry/brook/tailwind/ui/background.tsx
"use client";
import type React from "react";
import { useEffect, useRef } from "react";
import { cn } from "@/lib/utils";

const vertexShader = `
  attribute vec2 a_position;
  varying vec2 v_uv;

  void main() {
    v_uv = a_position * 0.5 + 0.5;
    gl_Position = vec4(a_position, 0.0, 1.0);
  }
`;

const fragmentShader = `
  precision highp float;

  uniform float u_time;
  uniform vec2 u_resolution;

  uniform vec3 u_primaryColor;
  uniform vec3 u_secondaryColor;
  uniform vec3 u_backgroundColor;
  uniform float u_backgroundOpacity;

  // Animation uniforms
  uniform float u_animationSpeed;
  uniform float u_noiseScale;
  uniform float u_intensity;

  uniform float u_octaves;
  uniform float u_warpStrength;
  varying vec2 v_uv;

  // Mathematical constants with high precision
  const float PI = 3.1415926535897932384626433832795;
  const float TAU = 6.2831853071795864769252867665590; // 2 * PI
  const float INV_PI = 0.3183098861837906715377675267450;
  const float SQRT_2 = 1.4142135623730950488016887242097;
  const float SQRT_3 = 1.7320508075688772935274463415059;
  const float PHI = 1.6180339887498948482045868343656; // Golden ratio

  // Simplex noise constants - mathematically derived for optimal distribution
  const float F2 = 0.36602540378443864676372317075294; // (sqrt(3) - 1) / 2
  const float G2 = 0.21132486540518713342105263157895; // (3 - sqrt(3)) / 6
  const float G2_2 = 0.42264973081037426684210526315789; // 2 * G2
  const float G2_MINUS_1 = -0.78867513459481286657894736842105; // 2 * G2 - 1

  // Improved permutation constants for better hash distribution
  const float PERM_MOD = 289.0;
  const float PERM_MUL = 34.0;
  const float PERM_ADD = 1.0;

  // Optimized normalization constant for better noise range
  const float NOISE_SCALE = 70.0; // Improved from 130.0 for better dynamic range

  // FBM configuration constants
  const float FBM_LACUNARITY = 2.0;      // Frequency multiplier between octaves
  const float FBM_PERSISTENCE = 0.5;     // Amplitude multiplier between octaves
  const float FBM_INITIAL_AMP = 0.5;     // Starting amplitude

  // Domain warping offset vectors - using irrational numbers for better distribution
  const vec2 WARP_OFFSET_1 = vec2(0.0, 0.0);
  const vec2 WARP_OFFSET_2 = vec2(5.12, 1.41); // Approximations of sqrt(26.2144) and sqrt(2)
  const vec2 WARP_OFFSET_3 = vec2(1.73, 9.17); // Approximations of sqrt(3) and sqrt(84.1)
  const vec2 WARP_OFFSET_4 = vec2(8.37, 2.83); // Approximations of sqrt(70.06) and sqrt(8)

  // Animation velocity constants - using phi-based ratios for organic motion
  const float ANIM_VEL_X1 = 0.1;
  const float ANIM_VEL_Y1 = 0.05;
  const float ANIM_VEL_X2 = -0.08;
  const float ANIM_VEL_Y2 = 0.12;
  const float ANIM_VEL_X3 = 0.06;
  const float ANIM_VEL_Y3 = -0.04;

  // Time scaling factors for warping
  const float WARP_TIME_SCALE_1 = 0.15;
  const float WARP_TIME_SCALE_2 = 0.126; // Approximation of 1/e^2

  // Layer scaling factors
  const float LAYER_SCALE_2 = 1.5;
  const float LAYER_SCALE_3 = 2.5;

  // Noise combination weights - normalized to sum to 1.0
  const float NOISE_WEIGHT_1 = 0.6;
  const float NOISE_WEIGHT_2 = 0.3;
  const float NOISE_WEIGHT_3 = 0.1;

  // Palette animation frequencies - using musical ratios
  const float PALETTE_FREQ_1 = 1.5;     // Perfect fifth ratio
  const float PALETTE_FREQ_2 = 2.25;    // Major ninth ratio
  const float PALETTE_AMP_1 = 0.4;
  const float PALETTE_AMP_2 = 0.3;

  // Background noise scales and intensities
  const float BG_NOISE_SCALE_1 = 2.0;
  const float BG_NOISE_SCALE_2 = 5.0;
  const float BG_NOISE_INTENSITY_1 = 0.02;
  const float BG_NOISE_INTENSITY_2 = 0.02;
  const float BG_NOISE_TIME_SCALE_1 = 0.1;
  const float BG_NOISE_TIME_SCALE_2 = 0.15;

  // Color mixing ratios
  const float COLOR_MIX_RATIO_1 = 0.6;
  const float COLOR_MIX_RATIO_2 = 0.7;
  const float COLOR_INTENSITY_MUL = 0.8;
  const float COLOR_WAVE_AMP = 0.6;

  // Smoothstep range for pattern definition
  const float PATTERN_LOW = -0.3;
  const float PATTERN_HIGH = 0.5;

  // Color shift parameters
  const float COLOR_SHIFT_TIME_SCALE = 0.2;
  const float COLOR_SHIFT_UV_SCALE = 0.4;
  const float COLOR_SHIFT_NOISE_SCALE = 0.3;

  // Improved permutation function with better distribution
  vec3 permute(vec3 x) {
      return mod(((x * PERM_MUL) + PERM_ADD) * x, PERM_MOD);
  }

  // Optimized Simplex noise with improved constants
  float snoise(vec2 v) {
      // Skew the input space to determine which simplex cell we're in
      vec2 i = floor(v + dot(v, vec2(F2)));
      vec2 x0 = v - i + dot(i, vec2(G2));

      // Determine which simplex we are in
      vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);

      // Offsets for second and third corner of simplex in (x,y) uv coords
      vec4 x12 = x0.xyxy + vec4(G2, G2, G2_MINUS_1, G2_MINUS_1);
      x12.xy -= i1;

      // Permutations
      i = mod(i, PERM_MOD);
      vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0));

      // Circularly symmetric blending kernel
      vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0);
      m = m * m * m * m; // Fourth power for smoother falloff

      // Gradients from 41 points on a line, mapped onto a diamond
      vec3 x = fract(p * (1.0 / 41.0)) * 2.0 - 1.0;
      vec3 h = abs(x) - 0.5;
      vec3 ox = floor(x + 0.5);
      vec3 a0 = x - ox;

      // Normalize gradients implicitly by scaling m
      m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h);

      // Compute final noise value at P
      vec3 g;
      g.x = a0.x * x0.x + h.x * x0.y;
      g.yz = a0.yz * x12.xz + h.yz * x12.yw;

      return NOISE_SCALE * dot(m, g);
  }

  // Improved FBM with proper normalization
  float fbm(vec2 pos, int octaves) {
      float value = 0.0;
      float amplitude = FBM_INITIAL_AMP;
      float frequency = 1.0;
      float maxValue = 0.0; // Used for normalization

      for (int i = 0; i < 6; i++) {
          if (i >= octaves) break;
          value += amplitude * snoise(pos * frequency);
          maxValue += amplitude;
          amplitude *= FBM_PERSISTENCE;
          frequency *= FBM_LACUNARITY;
      }

      // Normalize to [-1, 1] range
      return value / maxValue;
  }

  // Enhanced domain warping with better offset distribution
  float domainWarp(vec2 pos, float time) {
      // Primary distortion layer
      vec2 q = vec2(
          fbm(pos + WARP_OFFSET_1, int(u_octaves)),
          fbm(pos + WARP_OFFSET_2, int(u_octaves))
      );

      // Secondary distortion layer with time animation
      vec2 r = vec2(
          fbm(pos + u_warpStrength * q + WARP_OFFSET_3 + WARP_TIME_SCALE_1 * time, int(u_octaves - 1.0)),
          fbm(pos + u_warpStrength * q + WARP_OFFSET_4 + WARP_TIME_SCALE_2 * time, int(u_octaves - 1.0))
      );

      // Final warped noise
      return fbm(pos + u_warpStrength * r, int(u_octaves - 2.0));
  }

  // Improved palette function with better color theory
  vec3 palette(float t) {
      vec3 a = u_primaryColor;
      vec3 b = u_secondaryColor;
      vec3 c = mix(u_primaryColor, u_secondaryColor, COLOR_MIX_RATIO_1);
      vec3 d = mix(u_primaryColor, u_secondaryColor, COLOR_MIX_RATIO_2);

      // Use sinusoidal modulation with musical frequency ratios
      d = mix(d, u_secondaryColor * 0.5, sin(t * PALETTE_FREQ_1) * PALETTE_AMP_1 + 0.5);
      d = mix(d, u_primaryColor * 1.2, sin(t * PALETTE_FREQ_2) * PALETTE_AMP_2 + 0.5);

      // Use precise TAU instead of approximation
      float wave = cos(TAU * (c.x * t + d.x)) * COLOR_WAVE_AMP;
      vec3 result = a + b * wave * COLOR_INTENSITY_MUL;

      return result;
  }

  void main() {
      vec2 uv = v_uv;
      vec2 st = uv * u_noiseScale;

      float time = u_time * u_animationSpeed;

      // Layer 1: Domain-warped noise with primary animation
      vec2 animPos = st + vec2(time * ANIM_VEL_X1, time * ANIM_VEL_Y1);
      float noise1 = domainWarp(animPos, time);

      // Layer 2: Standard FBM with secondary animation
      vec2 animPos2 = st * LAYER_SCALE_2 + vec2(time * ANIM_VEL_X2, time * ANIM_VEL_Y2);
      float noise2 = fbm(animPos2, int(u_octaves - 1.0));

      // Layer 3: Simple noise with tertiary animation
      vec2 animPos3 = st * LAYER_SCALE_3 + vec2(time * ANIM_VEL_X3, time * ANIM_VEL_Y3);
      float noise3 = snoise(animPos3);

      // Combine noise layers with normalized weights
      float combinedNoise = noise1 * NOISE_WEIGHT_1 + noise2 * NOISE_WEIGHT_2 + noise3 * NOISE_WEIGHT_3;

      // Create pattern with smooth transitions
      float pattern = smoothstep(PATTERN_LOW, PATTERN_HIGH, combinedNoise);

      // Generate color shift with multiple influences
      float colorShift = time * COLOR_SHIFT_TIME_SCALE + uv.x * COLOR_SHIFT_UV_SCALE;
      colorShift += noise1 * COLOR_SHIFT_NOISE_SCALE;
      vec3 color = palette(colorShift);

      // Add subtle background noise
      vec3 bgNoise = vec3(snoise(uv * BG_NOISE_SCALE_1 + time * BG_NOISE_TIME_SCALE_1) * BG_NOISE_INTENSITY_1);
      vec3 bgColor = u_backgroundColor + bgNoise;

      // Apply pattern intensity
      float intensity = pattern * u_intensity;
      color = mix(bgColor, color, intensity);

      // Add background movement texture
      vec3 bgMovement = vec3(snoise(uv * BG_NOISE_SCALE_2 + time * BG_NOISE_TIME_SCALE_2) * BG_NOISE_INTENSITY_2);
      color += bgMovement;

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

type BackgroundProps = {
  className?: string;
  primaryColor?: [number, number, number];
  secondaryColor?: [number, number, number];
  backgroundColor?: [number, number, number];
  backgroundOpacity?: number;
  animationSpeed?: number;
  noiseScale?: number;
  intensity?: number;
  octaves?: number;
  warpStrength?: number;
};

const Background: React.FC<BackgroundProps> = ({
  className,
  // biome-ignore lint/style/noMagicNumbers: WebGL RGB color values
  primaryColor = [0.85, 0.85, 0.85],
  // biome-ignore lint/style/noMagicNumbers: WebGL RGB color values
  secondaryColor = [0.25, 0.25, 0.25],
  // biome-ignore lint/style/noMagicNumbers: WebGL RGB color values
  backgroundColor = [0.0, 0.0, 0.0],
  backgroundOpacity = 0.0,
  animationSpeed = 0.3,
  noiseScale = 3.0,
  intensity = 0.9,
  octaves = 4,
  warpStrength = 4.0,
}) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const animationRef = useRef<number>(0);
  const startTimeRef = useRef<number>(0);

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

    const gl = canvas.getContext("webgl");
    if (!gl) {
      return;
    }

    // Set canvas size
    const setCanvasSize = () => {
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      gl.viewport(0, 0, canvas.width, canvas.height);
    };

    setCanvasSize();
    window.addEventListener("resize", setCanvasSize);

    // Create shaders
    const createShader = (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)) {
        gl.deleteShader(shader);
        return null;
      }

      return shader;
    };

    const vertShader = createShader(gl.VERTEX_SHADER, vertexShader);
    const fragShader = createShader(gl.FRAGMENT_SHADER, fragmentShader);

    if (!(vertShader && fragShader)) {
      return;
    }

    // Create program
    const program = gl.createProgram();
    if (!program) {
      return;
    }

    gl.attachShader(program, vertShader);
    gl.attachShader(program, fragShader);
    gl.linkProgram(program);

    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
      return;
    }

    // Set up geometry
    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 positionLocation = gl.getAttribLocation(program, "a_position");
    gl.enableVertexAttribArray(positionLocation);
    gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);

    // Get uniform locations
    const timeLocation = gl.getUniformLocation(program, "u_time");
    const resolutionLocation = gl.getUniformLocation(program, "u_resolution");

    const primaryColorLocation = gl.getUniformLocation(program, "u_primaryColor");
    const secondaryColorLocation = gl.getUniformLocation(program, "u_secondaryColor");
    const backgroundColorLocation = gl.getUniformLocation(program, "u_backgroundColor");
    const backgroundOpacityLocation = gl.getUniformLocation(program, "u_backgroundOpacity");

    const animationSpeedLocation = gl.getUniformLocation(program, "u_animationSpeed");
    const noiseScaleLocation = gl.getUniformLocation(program, "u_noiseScale");
    const intensityLocation = gl.getUniformLocation(program, "u_intensity");

    const octavesLocation = gl.getUniformLocation(program, "u_octaves");
    const warpStrengthLocation = gl.getUniformLocation(program, "u_warpStrength");

    const MILLISECONDS_TO_SECONDS = 0.001;
    const TRIANGLE_STRIP_VERTICES = 4;

    const render = (time: number) => {
      if (!startTimeRef.current) {
        startTimeRef.current = time;
      }
      const elapsedTime = (time - startTimeRef.current) * MILLISECONDS_TO_SECONDS;

      // biome-ignore lint/correctness/useHookAtTopLevel: gl.useProgram is WebGL API, not a React hook
      gl.useProgram(program);

      gl.uniform1f(timeLocation, elapsedTime);
      gl.uniform2f(resolutionLocation, canvas.width, canvas.height);

      gl.uniform3f(primaryColorLocation, primaryColor[0], primaryColor[1], primaryColor[2]);
      gl.uniform3f(secondaryColorLocation, secondaryColor[0], secondaryColor[1], secondaryColor[2]);
      gl.uniform3f(backgroundColorLocation, backgroundColor[0], backgroundColor[1], backgroundColor[2]);
      gl.uniform1f(backgroundOpacityLocation, backgroundOpacity);

      gl.uniform1f(animationSpeedLocation, animationSpeed);
      gl.uniform1f(noiseScaleLocation, noiseScale);
      gl.uniform1f(intensityLocation, intensity);

      gl.uniform1f(octavesLocation, octaves);
      gl.uniform1f(warpStrengthLocation, warpStrength);

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

      animationRef.current = requestAnimationFrame(render);
    };

    render(0);

    return () => {
      window.removeEventListener("resize", setCanvasSize);
      if (animationRef.current) {
        cancelAnimationFrame(animationRef.current);
      }
    };
  }, [
    primaryColor,
    secondaryColor,
    backgroundColor,
    backgroundOpacity,
    animationSpeed,
    noiseScale,
    intensity,
    octaves,
    warpStrength,
  ]);

  return (
    <div className={cn("relative h-full w-full overflow-hidden rounded-[var(--radius)]", className)}>
      <canvas className="block h-full w-full object-cover" ref={canvasRef} />
    </div>
  );
};

export default Background;

Installation

npx shadcn@latest add @roiui/background-tailwind

Usage

import { BackgroundTailwind } from "@/components/background-tailwind"
<BackgroundTailwind />