Files
lidify/frontend/components/ui/GalaxyBackground.tsx
2025-12-25 18:58:06 -06:00

177 lines
7.4 KiB
TypeScript

"use client";
import { useMemo } from "react";
/**
* GalaxyBackground Component
*
* Creates a cosmic background effect that fades up from the bottom of the page.
* Features:
* - Subtle gradient fading from bottom (prominent) to top (transparent)
* - Floating star-like particles
* - More prominent at the bottom, fading as it goes higher
* - Customizable colors from Vibrant.js for artist/album pages
*/
interface GalaxyBackgroundProps {
/** Primary color extracted from Vibrant.js (e.g., "#8B4789") */
primaryColor?: string;
/** Optional secondary color */
secondaryColor?: string;
}
export function GalaxyBackground({ primaryColor, secondaryColor }: GalaxyBackgroundProps = {}) {
// Convert hex color to RGB values for opacity control
const hexToRgb = (hex: string) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
};
// Use provided colors or default purple theme
const baseColor = primaryColor ? hexToRgb(primaryColor) : null;
const accentColor = secondaryColor ? hexToRgb(secondaryColor) : null;
// Memoize particle positions so they don't regenerate on every render
const particles = useMemo(() => ({
bottom: Array(30).fill(0).map(() => ({
left: Math.random() * 100,
bottom: Math.random() * 30,
duration: 3 + Math.random() * 4,
delay: Math.random() * 3,
})),
mid: Array(20).fill(0).map(() => ({
left: Math.random() * 100,
bottom: 30 + Math.random() * 30,
duration: 4 + Math.random() * 3,
delay: Math.random() * 2,
})),
top: Array(12).fill(0).map(() => ({
left: Math.random() * 100,
bottom: 60 + Math.random() * 40,
duration: 5 + Math.random() * 3,
delay: Math.random() * 2,
})),
white: Array(18).fill(0).map(() => ({
left: Math.random() * 100,
bottom: Math.random() * 50,
duration: 2 + Math.random() * 3,
delay: Math.random() * 2,
})),
accent: Array(10).fill(0).map(() => ({
left: Math.random() * 100,
bottom: Math.random() * 40,
duration: 4 + Math.random() * 4,
delay: Math.random() * 3,
})),
}), []); // Empty dependency array - generate once and never again
return (
<div className="fixed inset-0 pointer-events-none z-0 overflow-hidden">
{/* Subtle gradient - fades from bottom to top */}
{baseColor ? (
<>
<div
className="absolute inset-0 bg-gradient-to-t to-transparent"
style={{
backgroundImage: `linear-gradient(to top, rgba(${baseColor.r}, ${baseColor.g}, ${baseColor.b}, 0.15), rgba(${baseColor.r}, ${baseColor.g}, ${baseColor.b}, 0.05), transparent)`
}}
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/40 via-black/10 to-transparent" />
</>
) : (
<>
<div className="absolute inset-0 bg-gradient-to-t from-purple-950/15 via-purple-950/5 to-transparent" />
<div className="absolute inset-0 bg-gradient-to-t from-black/40 via-black/10 to-transparent" />
</>
)}
{/* Floating Star Particles - more concentrated at bottom */}
{/* Bottom layer - most prominent */}
{particles.bottom.map((p, i) => (
<div
key={`bottom-purple-${i}`}
className={baseColor ? "absolute w-0.5 h-0.5 rounded-full blur-[0.4px]" : "absolute w-0.5 h-0.5 bg-purple-300/35 rounded-full blur-[0.4px]"}
style={{
left: `${p.left}%`,
bottom: `${p.bottom}%`,
animation: `galaxyFloat ${p.duration}s ease-in-out infinite`,
animationDelay: `${p.delay}s`,
...(baseColor && {
backgroundColor: `rgba(${baseColor.r}, ${baseColor.g}, ${baseColor.b}, 0.35)`
})
}}
/>
))}
{/* Middle layer - medium prominence */}
{particles.mid.map((p, i) => (
<div
key={`mid-purple-${i}`}
className={baseColor ? "absolute w-0.5 h-0.5 rounded-full blur-[0.4px]" : "absolute w-0.5 h-0.5 bg-indigo-300/25 rounded-full blur-[0.4px]"}
style={{
left: `${p.left}%`,
bottom: `${p.bottom}%`,
animation: `galaxyFloat ${p.duration}s ease-in-out infinite`,
animationDelay: `${p.delay}s`,
...(baseColor && {
backgroundColor: `rgba(${baseColor.r}, ${baseColor.g}, ${baseColor.b}, 0.25)`
})
}}
/>
))}
{/* Top layer - subtle and sparse */}
{particles.top.map((p, i) => (
<div
key={`top-purple-${i}`}
className={baseColor ? "absolute w-0.5 h-0.5 rounded-full blur-[0.4px]" : "absolute w-0.5 h-0.5 bg-violet-300/15 rounded-full blur-[0.4px]"}
style={{
left: `${p.left}%`,
bottom: `${p.bottom}%`,
animation: `galaxyFloat ${p.duration}s ease-in-out infinite`,
animationDelay: `${p.delay}s`,
...(baseColor && {
backgroundColor: `rgba(${baseColor.r}, ${baseColor.g}, ${baseColor.b}, 0.15)`
})
}}
/>
))}
{/* Accent white/blue stars scattered throughout */}
{particles.white.map((p, i) => (
<div
key={`white-star-${i}`}
className="absolute w-0.5 h-0.5 bg-white/30 rounded-full blur-[0.3px]"
style={{
left: `${p.left}%`,
bottom: `${p.bottom}%`,
animation: `galaxyTwinkle ${p.duration}s ease-in-out infinite`,
animationDelay: `${p.delay}s`,
}}
/>
))}
{/* Very subtle accent particles - use secondary color if available */}
{particles.accent.map((p, i) => (
<div
key={`blue-accent-${i}`}
className={accentColor ? "absolute w-0.5 h-0.5 rounded-full blur-[0.4px]" : "absolute w-0.5 h-0.5 bg-blue-300/25 rounded-full blur-[0.4px]"}
style={{
left: `${p.left}%`,
bottom: `${p.bottom}%`,
animation: `galaxyFloat ${p.duration}s ease-in-out infinite`,
animationDelay: `${p.delay}s`,
...(accentColor && {
backgroundColor: `rgba(${accentColor.r}, ${accentColor.g}, ${accentColor.b}, 0.25)`
})
}}
/>
))}
</div>
);
}