Black Hole
A WebGL black hole effect with accretion disc, orbiting particles, gravitational lensing distortion, and chromatic aberration.
"use client";
import { useState } from "react";
import { ChevronUp } from "lucide-react";
import BlackHole from "@/components/vritti/black-hole";
import { Slider } from "@/components/vritti/slider";
import { Label } from "@/components/vritti/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/vritti/select";
import { Button } from "@/components/vritti/button";
export function BlackHoleExample() {
const [open, setOpen] = useState(false);
const [innerColor, setInnerColor] = useState("#ff8080");
const [outerColor, setOuterColor] = useState("#3633ff");
const [rotationSpeed, setRotationSpeed] = useState(0.3);
const [orbitSpeed, setOrbitSpeed] = useState(1.0);
const [particleSize, setParticleSize] = useState(0.015);
const [distortionStrength, setDistortionStrength] = useState(1.0);
const [rgbShiftRadius, setRgbShiftRadius] = useState(0.002);
const [quality, setQuality] = useState<"low" | "medium" | "high">("medium");
return (
<div className="relative h-[600px] w-full overflow-hidden rounded-lg border border-border bg-background">
<BlackHole
innerColor={innerColor}
outerColor={outerColor}
quality={quality}
rotationSpeed={rotationSpeed}
orbitSpeed={orbitSpeed}
particleSize={particleSize}
distortionStrength={distortionStrength}
rgbShiftRadius={rgbShiftRadius}
/>
{/* Controls toggle */}
<Button
variant="secondary"
size="sm"
onClick={() => setOpen(!open)}
className="absolute bottom-3 left-1/2 z-20 -translate-x-1/2 gap-1.5 bg-background/80 backdrop-blur-sm text-xs"
>
<ChevronUp
className={`h-3.5 w-3.5 transition-transform ${open ? "rotate-180" : ""}`}
/>
{open ? "Hide" : "Show"} Controls
</Button>
{/* Controls panel */}
{open && (
<div className="absolute bottom-10 left-0 right-0 z-10 border-t border-border bg-background/90 backdrop-blur-md px-5 pb-5 pt-4">
<div className="grid grid-cols-2 gap-x-6 gap-y-4 sm:grid-cols-4">
{/* Inner Color */}
<div className="flex flex-col gap-2">
<Label className="text-xs text-muted-foreground">Inner Color</Label>
<div className="flex items-center gap-2">
<div className="relative h-8 w-8 shrink-0">
<div
className="h-8 w-8 rounded-md border border-border"
style={{ background: innerColor }}
/>
<input
type="color"
value={innerColor}
onChange={(e) => setInnerColor(e.target.value)}
className="absolute inset-0 h-full w-full cursor-pointer opacity-0"
/>
</div>
<span className="font-mono text-xs text-muted-foreground">
{innerColor}
</span>
</div>
</div>
{/* Outer Color */}
<div className="flex flex-col gap-2">
<Label className="text-xs text-muted-foreground">Outer Color</Label>
<div className="flex items-center gap-2">
<div className="relative h-8 w-8 shrink-0">
<div
className="h-8 w-8 rounded-md border border-border"
style={{ background: outerColor }}
/>
<input
type="color"
value={outerColor}
onChange={(e) => setOuterColor(e.target.value)}
className="absolute inset-0 h-full w-full cursor-pointer opacity-0"
/>
</div>
<span className="font-mono text-xs text-muted-foreground">
{outerColor}
</span>
</div>
</div>
{/* Rotation Speed */}
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<Label className="text-xs text-muted-foreground">Rotation Speed</Label>
<span className="font-mono text-xs text-foreground">
{rotationSpeed.toFixed(1)}
</span>
</div>
<Slider
value={[rotationSpeed]}
onValueChange={([v]) => setRotationSpeed(v)}
min={0}
max={2}
step={0.1}
/>
</div>
{/* Orbit Speed */}
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<Label className="text-xs text-muted-foreground">Orbit Speed</Label>
<span className="font-mono text-xs text-foreground">
{orbitSpeed.toFixed(1)}
</span>
</div>
<Slider
value={[orbitSpeed]}
onValueChange={([v]) => setOrbitSpeed(v)}
min={0}
max={3}
step={0.1}
/>
</div>
{/* Particle Size */}
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<Label className="text-xs text-muted-foreground">Particle Size</Label>
<span className="font-mono text-xs text-foreground">
{particleSize.toFixed(3)}
</span>
</div>
<Slider
value={[particleSize]}
onValueChange={([v]) => setParticleSize(v)}
min={0.005}
max={0.05}
step={0.005}
/>
</div>
{/* Distortion */}
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<Label className="text-xs text-muted-foreground">Distortion</Label>
<span className="font-mono text-xs text-foreground">
{distortionStrength.toFixed(1)}
</span>
</div>
<Slider
value={[distortionStrength]}
onValueChange={([v]) => setDistortionStrength(v)}
min={0}
max={5}
step={0.1}
/>
</div>
{/* RGB Shift */}
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<Label className="text-xs text-muted-foreground">RGB Shift</Label>
<span className="font-mono text-xs text-foreground">
{rgbShiftRadius.toFixed(3)}
</span>
</div>
<Slider
value={[rgbShiftRadius]}
onValueChange={([v]) => setRgbShiftRadius(v)}
min={0}
max={0.02}
step={0.001}
/>
</div>
{/* Quality */}
<div className="flex flex-col gap-2">
<Label className="text-xs text-muted-foreground">Quality</Label>
<Select
value={quality}
onValueChange={(v) =>
setQuality(v as "low" | "medium" | "high")
}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="low">Low</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="high">High</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
)}
</div>
);
}
Installation
pnpm dlx shadcn@latest add "https://vritti.thesatyajit.com/r/black-hole"
Dependencies
pnpm add three @types/three