Add particle background effect

- Pure CSS/JS particle animation behind all content
- Multiple layers with different speeds for depth effect
- Theme-aware: white particles on dark mode, purple on light mode
- Low opacity for subtle, non-distracting ambiance
- Uses box-shadow technique for performance

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
clucraft 2026-01-24 03:41:02 -05:00
parent c4c05236c0
commit 2bbaab8793
2 changed files with 111 additions and 0 deletions

View file

@ -2,6 +2,7 @@ import { ReactNode, useState, useEffect, useRef } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';
import NotificationBell from './NotificationBell';
import ParticleBackground from './ParticleBackground';
interface LayoutProps {
children: ReactNode;
@ -293,6 +294,8 @@ export default function Layout({ children }: LayoutProps) {
}
`}</style>
<ParticleBackground />
<nav className="navbar">
<div className="navbar-content">
<Link to="/" className="navbar-brand">

View file

@ -0,0 +1,108 @@
import { useMemo } from 'react';
// Generate random box-shadow particles
function generateParticles(count: number, spacing: number, color: string): string {
const shadows: string[] = [];
for (let i = 0; i < count; i++) {
const x = Math.floor(Math.random() * spacing);
const y = Math.floor(Math.random() * spacing);
shadows.push(`${x}px ${y}px ${color}`);
}
return shadows.join(', ');
}
interface ParticleLayerProps {
count: number;
size: number;
duration: number;
color: string;
spacing: number;
}
function ParticleLayer({ count, size, duration, color, spacing }: ParticleLayerProps) {
const boxShadow = useMemo(() => generateParticles(count, spacing, color), [count, spacing, color]);
const boxShadowAfter = useMemo(() => generateParticles(Math.floor(count * 0.8), spacing, color), [count, spacing, color]);
return (
<div
className="particle-layer"
style={{
position: 'absolute',
top: 0,
left: 0,
width: `${size}px`,
height: `${size}px`,
background: 'transparent',
boxShadow,
borderRadius: '50%',
animation: `particleFloat ${duration}s linear infinite`,
}}
>
<div
style={{
position: 'absolute',
top: `${spacing}px`,
left: 0,
width: `${size}px`,
height: `${size}px`,
background: 'transparent',
boxShadow: boxShadowAfter,
borderRadius: '50%',
}}
/>
</div>
);
}
export default function ParticleBackground() {
const spacing = 2000;
return (
<div className="particle-background">
<style>{`
.particle-background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
pointer-events: none;
overflow: hidden;
}
@keyframes particleFloat {
from {
transform: translateY(0px);
}
to {
transform: translateY(-${spacing}px);
}
}
/* Light mode - subtle gray particles */
[data-theme="light"] .particle-background {
opacity: 0.3;
}
[data-theme="light"] .particle-layer {
--particle-color: #6366f1;
}
/* Dark mode - white particles */
[data-theme="dark"] .particle-background {
opacity: 0.4;
}
[data-theme="dark"] .particle-layer {
--particle-color: #ffffff;
}
`}</style>
{/* Multiple layers with different speeds for depth effect */}
<ParticleLayer count={300} size={1} duration={80} color="var(--particle-color, #fff)" spacing={spacing} />
<ParticleLayer count={200} size={2} duration={120} color="var(--particle-color, #fff)" spacing={spacing} />
<ParticleLayer count={100} size={2} duration={160} color="var(--particle-color, #fff)" spacing={spacing} />
</div>
);
}