142 lines
No EOL
4.8 KiB
TypeScript
142 lines
No EOL
4.8 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
import { useUIStore } from '@/stores/ui';
|
|
|
|
/**
|
|
* Composant de fond animé "Astral/Ceramic"
|
|
* S'adapte intelligemment au thème (Étoiles en Dark, Particules d'encre en Light)
|
|
*/
|
|
export function AstralBackground() {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
const { theme } = useUIStore(); // Hook pour détecter le changement de thème
|
|
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
// Détection du mode clair via la classe sur html/body ou le store
|
|
const isLightMode = document.documentElement.classList.contains('light');
|
|
|
|
let animationFrameId: number;
|
|
let particles: {
|
|
x: number;
|
|
y: number;
|
|
size: number;
|
|
speedX: number;
|
|
speedY: number;
|
|
opacity: number;
|
|
}[] = [];
|
|
|
|
// Configuration adaptative
|
|
const particleCount = window.innerWidth < 768 ? 20 : 50; // Performance mobile
|
|
const connectionDistance = 150;
|
|
|
|
// Couleurs basées sur le thème (Hardcoded pour perf canvas, mais aligné avec design-tokens)
|
|
const particleColor = isLightMode ? '14, 165, 233' : '102, 252, 241'; // Sky Blue vs Cyan Neon
|
|
const lineColor = isLightMode ? '148, 163, 184' : '102, 252, 241'; // Slate vs Cyan
|
|
|
|
const resize = () => {
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
};
|
|
|
|
const createParticles = () => {
|
|
particles = [];
|
|
for (let i = 0; i < particleCount; i++) {
|
|
particles.push({
|
|
x: Math.random() * canvas.width,
|
|
y: Math.random() * canvas.height,
|
|
size: Math.random() * (isLightMode ? 3 : 2), // Particules un peu plus grosses en light (effet encre)
|
|
speedX: (Math.random() - 0.5) * 0.2,
|
|
speedY: (Math.random() - 0.5) * 0.2,
|
|
opacity: Math.random() * 0.5 + 0.1,
|
|
});
|
|
}
|
|
};
|
|
|
|
const animate = () => {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Update and draw particles
|
|
particles.forEach((p, i) => {
|
|
p.x += p.speedX;
|
|
p.y += p.speedY;
|
|
|
|
// Wrap around screen
|
|
if (p.x < 0) p.x = canvas.width;
|
|
if (p.x > canvas.width) p.x = 0;
|
|
if (p.y < 0) p.y = canvas.height;
|
|
if (p.y > canvas.height) p.y = 0;
|
|
|
|
// Draw particle
|
|
ctx.beginPath();
|
|
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
|
|
ctx.fillStyle = `rgba(${particleColor}, ${p.opacity})`;
|
|
ctx.fill();
|
|
|
|
// Draw connections
|
|
for (let j = i + 1; j < particles.length; j++) {
|
|
const p2 = particles[j];
|
|
const dx = p.x - p2.x;
|
|
const dy = p.y - p2.y;
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (distance < connectionDistance) {
|
|
ctx.beginPath();
|
|
// Lignes beaucoup plus subtiles en mode clair
|
|
const lineOpacity = isLightMode ? 0.05 : 0.1;
|
|
ctx.strokeStyle = `rgba(${lineColor}, ${lineOpacity * (1 - distance / connectionDistance)})`;
|
|
ctx.lineWidth = 0.5;
|
|
ctx.moveTo(p.x, p.y);
|
|
ctx.lineTo(p2.x, p2.y);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
});
|
|
|
|
animationFrameId = requestAnimationFrame(animate);
|
|
};
|
|
|
|
window.addEventListener('resize', resize);
|
|
resize();
|
|
createParticles();
|
|
animate();
|
|
|
|
return () => {
|
|
window.removeEventListener('resize', resize);
|
|
cancelAnimationFrame(animationFrameId);
|
|
};
|
|
}, [theme]); // Re-run effect when theme changes
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-0 pointer-events-none overflow-hidden transition-colors duration-700">
|
|
{/* Background Layer 1: Solid Base */}
|
|
<div className="absolute inset-0 bg-background transition-colors duration-700" />
|
|
|
|
{/* Background Layer 2: Subtle Gradient Vignette */}
|
|
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-black/20 dark:to-black/80" />
|
|
|
|
{/* Nebulas - Optimized opacity for themes */}
|
|
<div className="absolute top-[-20%] left-[-10%] w-[60%] h-[60%] rounded-full bg-cyan/5 dark:bg-cyan/5 blur-[120px] animate-pulse" />
|
|
<div
|
|
className="absolute bottom-[-20%] right-[-10%] w-[60%] h-[60%] rounded-full bg-magenta/5 dark:bg-magenta/5 blur-[120px] animate-pulse"
|
|
style={{ animationDelay: '2s' }}
|
|
/>
|
|
|
|
{/* Star Field Canvas */}
|
|
<canvas ref={canvasRef} className="absolute inset-0 opacity-60 dark:opacity-40" />
|
|
|
|
{/* Grid Overlay - Very subtle, helps grounding */}
|
|
<div
|
|
className="absolute inset-0 opacity-[0.02] dark:opacity-[0.03]"
|
|
style={{
|
|
backgroundImage:
|
|
'linear-gradient(rgb(var(--sidebar-border)) 1px, transparent 1px)',
|
|
backgroundSize: '100px 100px',
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
} |