---
title: Ai Aurora Blob
description: A gorgeous WebGL OGL 3D Perlin noise Soft Aurora fluid wave sphere component with real-time mouse interaction and customizable cosine gradients for AI voice assistants.
---
## Installation
CLI
Manual
```bash
npx lightswind@latest add ai-aurora-blob
```
Copy and paste the following code into your project.
```tsx
"use client";
import React, { useEffect, useRef, useState } from "react";
import { Renderer, Program, Mesh, Triangle } from "ogl";
import { motion } from "framer-motion";
import { MicOff } from "lucide-react";
function hexToVec3(hex: string): [number, number, number] {
const h = hex.replace("#", "");
return [
parseInt(h.slice(0, 2), 16) / 255 || 0,
parseInt(h.slice(2, 4), 16) / 255 || 0,
parseInt(h.slice(4, 6), 16) / 255 || 0,
];
}
const vertexShader = `#version 300 es
in vec2 uv;
in vec2 position;
out vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 0, 1);
}
`;
const fragmentShader = `#version 300 es
precision highp float;
uniform float uTime;
uniform vec3 uResolution;
uniform float uSpeed;
uniform float uScale;
uniform float uBrightness;
uniform vec3 uColor1;
uniform vec3 uColor2;
uniform float uNoiseFreq;
uniform float uNoiseAmp;
uniform float uBandHeight;
uniform float uBandSpread;
uniform float uOctaveDecay;
uniform float uLayerOffset;
uniform float uColorSpeed;
uniform vec2 uMouse;
uniform float uMouseInfluence;
uniform bool uEnableMouse;
uniform bool uShowBorder;
out vec4 fragColor;
#define TAU 6.28318530718
vec3 gradientHash(vec3 p) {
p = vec3(
dot(p, vec3(127.1, 311.7, 234.6)),
dot(p, vec3(269.5, 183.3, 198.3)),
dot(p, vec3(169.5, 283.3, 156.9))
);
vec3 h = fract(sin(p) * 43758.5453123);
float phi = acos(2.0 * h.x - 1.0);
float theta = TAU * h.y;
return vec3(cos(theta) * sin(phi), sin(theta) * cos(phi), cos(phi));
}
float quinticSmooth(float t) {
float t2 = t * t;
float t3 = t * t2;
return 6.0 * t3 * t2 - 15.0 * t2 * t2 + 10.0 * t3;
}
vec3 cosineGradient(float t, vec3 a, vec3 b, vec3 c, vec3 d) {
return a + b * cos(TAU * (c * t + d));
}
float perlin3D(float amplitude, float frequency, float px, float py, float pz) {
float x = px * frequency;
float y = py * frequency;
float fx = floor(x); float fy = floor(y); float fz = floor(pz);
float cx = ceil(x); float cy = ceil(y); float cz = ceil(pz);
vec3 g000 = gradientHash(vec3(fx, fy, fz));
vec3 g100 = gradientHash(vec3(cx, fy, fz));
vec3 g010 = gradientHash(vec3(fx, cy, fz));
vec3 g110 = gradientHash(vec3(cx, cy, fz));
vec3 g001 = gradientHash(vec3(fx, fy, cz));
vec3 g101 = gradientHash(vec3(cx, fy, cz));
vec3 g011 = gradientHash(vec3(fx, cy, cz));
vec3 g111 = gradientHash(vec3(cx, cy, cz));
float d000 = dot(g000, vec3(x - fx, y - fy, pz - fz));
float d100 = dot(g100, vec3(x - cx, y - fy, pz - fz));
float d010 = dot(g010, vec3(x - fx, y - cy, pz - fz));
float d110 = dot(g110, vec3(x - cx, y - cy, pz - fz));
float d001 = dot(g001, vec3(x - fx, y - fy, pz - cz));
float d101 = dot(g101, vec3(x - cx, y - fy, pz - cz));
float d011 = dot(g011, vec3(x - fx, y - cy, pz - cz));
float d111 = dot(g111, vec3(x - cx, y - cy, pz - cz));
float sx = quinticSmooth(x - fx);
float sy = quinticSmooth(y - fy);
float sz = quinticSmooth(pz - fz);
float lx00 = mix(d000, d100, sx);
float lx10 = mix(d010, d110, sx);
float lx01 = mix(d001, d101, sx);
float lx11 = mix(d011, d111, sx);
float ly0 = mix(lx00, lx10, sy);
float ly1 = mix(lx01, lx11, sy);
return amplitude * mix(ly0, ly1, sz);
}
float auroraGlow(float t, vec2 shift) {
vec2 uv = gl_FragCoord.xy / uResolution.y;
uv += shift;
float noiseVal = 0.0;
float freq = uNoiseFreq;
float amp = uNoiseAmp;
vec2 samplePos = uv * uScale;
for (float i = 0.0; i < 3.0; i += 1.0) {
noiseVal += perlin3D(amp, freq, samplePos.x, samplePos.y, t);
amp *= uOctaveDecay;
freq *= 2.0;
}
float yBand = (uv.y - 0.5) * 5.0;
return max(exp(-2.2 * abs(noiseVal + yBand * 0.45)), 0.0);
}
void main() {
vec2 uv = gl_FragCoord.xy / uResolution.xy;
float t = uSpeed * 0.4 * uTime;
vec2 p = (gl_FragCoord.xy - 0.5 * uResolution.xy) / uResolution.y;
float dist = length(p);
float radius = 0.36;
float edge = fwidth(dist) * 1.5;
float mask = 1.0 - smoothstep(radius - edge, radius + edge, dist);
if (mask <= 0.0) {
fragColor = vec4(0.0);
return;
}
vec2 shift = vec2(0.0);
if (uEnableMouse) {
shift = (uMouse - 0.5) * uMouseInfluence;
}
// 1. Dynamic Aurora Waves Field
vec3 auroraCol = vec3(0.0);
auroraCol += 0.95 * auroraGlow(t, shift) * cosineGradient(uv.x + uTime * uSpeed * 0.2 * uColorSpeed, vec3(0.5), vec3(0.5), vec3(1.0), vec3(0.3, 0.20, 0.20)) * uColor1;
auroraCol += 0.95 * auroraGlow(t + uLayerOffset, shift) * cosineGradient(uv.x + uTime * uSpeed * 0.1 * uColorSpeed, vec3(0.5), vec3(0.5), vec3(2.0, 1.0, 0.0), vec3(0.5, 0.20, 0.25)) * uColor2;
// 2. Full Sphere Ambient Fill (renders whole circular ball filled with dynamic aurora glow)
float normDist = clamp(dist / radius, 0.0, 1.0);
float centerVol = sqrt(max(1.0 - normDist * normDist, 0.0));
vec3 sphereBg = mix(uColor1, uColor2, sin(uv.x * 3.1415 + t * 0.5) * 0.5 + 0.5) * 0.45 * centerVol;
vec3 col = sphereBg + auroraCol * uBrightness;
// 3. Mild Frosted Glass Rim Reflection & Border Toggle (No dark shadow)
if (uShowBorder) {
float borderAngle = atan(p.y, p.x);
vec3 glassTint = mix(uColor1, uColor2, sin(borderAngle * 2.0 + t * 1.2) * 0.5 + 0.5);
vec3 glassHighlight = mix(vec3(1.0), glassTint * 1.2, 0.35);
// Sleek mild frosted glass rim highlight along outer radius (0.91 - 0.99)
float glassRim = sin(smoothstep(0.91, 0.99, normDist) * 3.14159);
// Blend mild crystal glass rim highlight
col = mix(col, glassHighlight, glassRim * 0.45);
}
// Soft spherical alpha falloff
float alpha = (0.75 + 0.25 * centerVol) * mask;
fragColor = vec4(col * alpha, alpha);
}
`;
export interface AiAuroraBlobProps extends React.HTMLAttributes {
/** Diameter of the AI Aurora Blob container in pixels (default: 260) */
size?: number;
/** Primary aurora gradient color (default: "#10b981") */
color1?: string;
/** Secondary aurora gradient color (default: "#6366f1") */
color2?: string;
/** AI Assistant state mode: "listening" | "thinking" | "speaking" | "orbit" | "pulse" */
variant?: "listening" | "thinking" | "speaking" | "orbit" | "pulse";
/** Aurora wave speed multiplier (default: 0.6) */
speed?: number;
/** Aurora wave scale (default: 1.5) */
scale?: number;
/** Aurora brightness (default: 1.2) */
brightness?: number;
/** Noise frequency (default: 2.5) */
noiseFrequency?: number;
/** Noise amplitude (default: 1.0) */
noiseAmplitude?: number;
/** Aurora band height position (default: 0.5) */
bandHeight?: number;
/** Aurora band spread (default: 1.0) */
bandSpread?: number;
/** Enable mouse interaction warping (default: true) */
enableMouseInteraction?: boolean;
/** Mouse warp influence factor (default: 0.25) */
mouseInfluence?: number;
/** Toggle outer glassmorphic border rim (default: true) */
showBorder?: boolean;
/** Active voice listening state */
isListening?: boolean;
/** Central mic click handler */
onMicClick?: () => void;
/** Custom content inside center orb */
centerContent?: React.ReactNode;
}
export const AiAuroraBlob: React.FC = ({
size = 260,
color1 = "#10b981",
color2 = "#6366f1",
variant = "listening",
speed = 0.6,
scale = 1.5,
brightness = 1.2,
noiseFrequency = 2.5,
noiseAmplitude = 1.0,
bandHeight = 0.5,
bandSpread = 1.0,
enableMouseInteraction = true,
mouseInfluence = 0.25,
showBorder = true,
isListening = true,
onMicClick,
centerContent,
className = "",
style,
onMouseEnter,
onMouseLeave,
...props
}) => {
const containerRef = useRef(null);
const [isHovered, setIsHovered] = useState(false);
// Dynamic state variations based on AI Voice Assistant mode
const variantConfig = React.useMemo(() => {
switch (variant) {
case "listening":
return { speedMult: 1.3, brightMult: 1.1, freqMult: 1.1 };
case "thinking":
return { speedMult: 1.9, brightMult: 1.3, freqMult: 1.8 };
case "speaking":
return { speedMult: 2.3, brightMult: 1.4, freqMult: 1.4 };
case "orbit":
return { speedMult: 0.8, brightMult: 1.0, freqMult: 0.9 };
case "pulse":
return { speedMult: 1.1, brightMult: 1.2, freqMult: 1.0 };
default:
return { speedMult: 1.0, brightMult: 1.0, freqMult: 1.0 };
}
}, [variant]);
const hoverSpeedMult = isHovered ? 2.4 : 1.0;
const hoverBrightMult = isHovered ? 1.25 : 1.0;
const propsRef = useRef({
speed: speed * variantConfig.speedMult * hoverSpeedMult,
scale,
brightness: brightness * variantConfig.brightMult * hoverBrightMult,
color1,
color2,
noiseFrequency: noiseFrequency * variantConfig.freqMult,
noiseAmplitude,
bandHeight,
bandSpread,
enableMouseInteraction,
mouseInfluence,
showBorder,
});
propsRef.current = {
speed: speed * variantConfig.speedMult * hoverSpeedMult,
scale,
brightness: brightness * variantConfig.brightMult * hoverBrightMult,
color1,
color2,
noiseFrequency: noiseFrequency * variantConfig.freqMult,
noiseAmplitude,
bandHeight,
bandSpread,
enableMouseInteraction,
mouseInfluence,
showBorder,
};
useEffect(() => {
const container = containerRef.current;
if (!container) return;
let renderer: Renderer | null = null;
try {
renderer = new Renderer({ alpha: true, premultipliedAlpha: true });
} catch (e) {
console.warn("WebGL not supported for AiAuroraBlob", e);
return;
}
const gl = renderer.gl;
gl.clearColor(0, 0, 0, 0);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
gl.canvas.style.backgroundColor = "transparent";
let program: Program;
let currentMouse = [0.5, 0.5];
let targetMouse = [0.5, 0.5];
function handleMouseMove(e: MouseEvent) {
const rect = gl.canvas.getBoundingClientRect();
targetMouse = [
(e.clientX - rect.left) / (rect.width || 1),
1.0 - (e.clientY - rect.top) / (rect.height || 1),
];
}
function handleMouseLeave() {
targetMouse = [0.5, 0.5];
}
function resize() {
if (!container || !renderer) return;
const w = container.offsetWidth || size;
const h = container.offsetHeight || size;
renderer.setSize(w, h);
if (program) {
program.uniforms.uResolution.value = [w, h, w / h];
}
}
const resizeObserver = new ResizeObserver(resize);
resizeObserver.observe(container);
resize();
const geometry = new Triangle(gl);
program = new Program(gl, {
vertex: vertexShader,
fragment: fragmentShader,
uniforms: {
uTime: { value: 0 },
uResolution: { value: [container.offsetWidth || size, container.offsetHeight || size, 1.0] },
uSpeed: { value: propsRef.current.speed },
uScale: { value: propsRef.current.scale },
uBrightness: { value: propsRef.current.brightness },
uColor1: { value: hexToVec3(propsRef.current.color1) },
uColor2: { value: hexToVec3(propsRef.current.color2) },
uNoiseFreq: { value: propsRef.current.noiseFrequency },
uNoiseAmp: { value: propsRef.current.noiseAmplitude },
uBandHeight: { value: propsRef.current.bandHeight },
uBandSpread: { value: propsRef.current.bandSpread },
uOctaveDecay: { value: 0.1 },
uLayerOffset: { value: 0.5 },
uColorSpeed: { value: 1.0 },
uMouse: { value: new Float32Array([0.5, 0.5]) },
uMouseInfluence: { value: propsRef.current.mouseInfluence },
uEnableMouse: { value: propsRef.current.enableMouseInteraction },
uShowBorder: { value: propsRef.current.showBorder },
},
});
const mesh = new Mesh(gl, { geometry, program });
container.appendChild(gl.canvas);
if (enableMouseInteraction) {
gl.canvas.addEventListener("mousemove", handleMouseMove);
gl.canvas.addEventListener("mouseleave", handleMouseLeave);
}
let animationFrameId: number;
function update(time: number) {
animationFrameId = requestAnimationFrame(update);
if (!renderer) return;
const current = propsRef.current;
program.uniforms.uTime.value = time * 0.001;
program.uniforms.uSpeed.value = current.speed;
program.uniforms.uScale.value = current.scale;
program.uniforms.uBrightness.value = current.brightness;
program.uniforms.uColor1.value = hexToVec3(current.color1);
program.uniforms.uColor2.value = hexToVec3(current.color2);
program.uniforms.uNoiseFreq.value = current.noiseFrequency;
program.uniforms.uNoiseAmp.value = current.noiseAmplitude;
program.uniforms.uBandHeight.value = current.bandHeight;
program.uniforms.uBandSpread.value = current.bandSpread;
program.uniforms.uEnableMouse.value = current.enableMouseInteraction;
program.uniforms.uMouseInfluence.value = current.mouseInfluence;
program.uniforms.uShowBorder.value = current.showBorder;
if (current.enableMouseInteraction) {
currentMouse[0] += 0.08 * (targetMouse[0] - currentMouse[0]);
currentMouse[1] += 0.08 * (targetMouse[1] - currentMouse[1]);
program.uniforms.uMouse.value[0] = currentMouse[0];
program.uniforms.uMouse.value[1] = currentMouse[1];
} else {
program.uniforms.uMouse.value[0] = 0.5;
program.uniforms.uMouse.value[1] = 0.5;
}
renderer.render({ scene: mesh });
}
animationFrameId = requestAnimationFrame(update);
return () => {
cancelAnimationFrame(animationFrameId);
resizeObserver.disconnect();
if (enableMouseInteraction && gl.canvas) {
gl.canvas.removeEventListener("mousemove", handleMouseMove);
gl.canvas.removeEventListener("mouseleave", handleMouseLeave);
}
if (container && gl.canvas.parentNode === container) {
container.removeChild(gl.canvas);
}
gl.getExtension("WEBGL_lose_context")?.loseContext();
};
}, [size]);
return (
{
setIsHovered(true);
onMouseEnter?.(e);
}}
onMouseLeave={(e) => {
setIsHovered(false);
onMouseLeave?.(e);
}}
className={`relative inline-flex items-center justify-center select-none overflow-hidden cursor-pointer group ${className}`}
style={{ width: size, height: size, ...style }}
{...props}
>
{/* ── Background WebGL Procedural Soft Aurora Waves ── */}
{/* ── Center Frosted Glassmorphic Voice Button ── */}
{centerContent ? (
centerContent
) : isListening ? (
{/* 1. LISTENING MODE: Dynamic Audio Equalizer Bounce */}
{variant === "listening" && (
<>
>
)}
{/* 2. THINKING MODE: Floating Loading Wave Dots */}
{variant === "thinking" && (
<>
>
)}
{/* 3. SPEAKING MODE: High Energy Rapid Speech Oscillation */}
{variant === "speaking" && (
<>
>
)}
{/* 4. ORBIT MODE: Rotating 3-Dot Orbital Loop */}
{variant === "orbit" && (
)}
{/* 5. PULSE MODE: Synchronized Breathing Equalizer Pulse */}
{variant === "pulse" && (
<>
>
)}
) : (
)}
);
};
export default AiAuroraBlob;
```
## Usage
```tsx
import { AiAuroraBlob } from "@/components/lightswind/ai-aurora-blob"
```
```tsx
import { AiAuroraBlob } from "@/components/lightswind/ai-aurora-blob";
export function Demo() {
return (
);
}
```