opencand.ui/src/components/MatrixBackground.tsx
José Henrique 222d25f1d2
All checks were successful
Frontend Build and Deploy / build (push) Successful in 21s
design improvments 2.0
2025-06-03 18:07:54 -03:00

348 lines
10 KiB
TypeScript

import React, { useEffect, useRef, useState } from 'react';
interface Dot {
x: number;
y: number;
vx: number;
vy: number;
connections: number[];
brightness: number;
targetBrightness: number;
}
interface Connection {
from: number;
to: number;
brightness: number;
targetBrightness: number;
thickness: number;
targetThickness: number;
}
const MatrixBackground: React.FC = () => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const animationRef = useRef<number>(0);
const dotsRef = useRef<Dot[]>([]);
const connectionsRef = useRef<Connection[]>([]);
const mouseRef = useRef({ x: 0, y: 0 });
const [, setDimensions] = useState({ width: 0, height: 0 });
// Configuration - subtle background for main content
const config = {
dotCount: 400,
maxConnections: 3,
connectionDistance: 150,
dotSpeed: 0.3,
hoverRadius: 120,
baseBrightness: 0.4, // Moderate base brightness for main background
hoverBrightness: 0.7, // Moderate hover brightness
baseThickness: 0.6,
hoverThickness: 1.8,
fadeSpeed: 0.08, // Slightly faster fade for better responsiveness
};
// Initialize dots
const initializeDots = (width: number, height: number) => {
const dots: Dot[] = [];
for (let i = 0; i < config.dotCount; i++) {
dots.push({
x: Math.random() * width,
y: Math.random() * height,
vx: (Math.random() - 0.5) * config.dotSpeed,
vy: (Math.random() - 0.5) * config.dotSpeed,
connections: [],
brightness: config.baseBrightness,
targetBrightness: config.baseBrightness,
});
}
return dots;
};
// Calculate connections between dots
const calculateConnections = (dots: Dot[]) => {
const connections: Connection[] = [];
for (let i = 0; i < dots.length; i++) {
const dot = dots[i];
dot.connections = [];
const distances: { index: number; distance: number }[] = [];
for (let j = 0; j < dots.length; j++) {
if (i === j) continue;
const dx = dot.x - dots[j].x;
const dy = dot.y - dots[j].y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < config.connectionDistance) {
distances.push({ index: j, distance });
}
}
// Sort by distance and take closest connections
distances.sort((a, b) => a.distance - b.distance);
const maxConnections = Math.min(config.maxConnections, distances.length);
for (let k = 0; k < maxConnections; k++) {
const targetIndex = distances[k].index;
// Avoid duplicate connections
const existing = connections.find(
conn => (conn.from === i && conn.to === targetIndex) ||
(conn.from === targetIndex && conn.to === i)
);
if (!existing) {
dot.connections.push(targetIndex);
connections.push({
from: i,
to: targetIndex,
brightness: config.baseBrightness,
targetBrightness: config.baseBrightness,
thickness: config.baseThickness,
targetThickness: config.baseThickness,
});
}
}
}
return connections;
};
// Update hover effects
const updateHoverEffects = (dots: Dot[], connections: Connection[], mouseX: number, mouseY: number) => {
// Reset all targets to base values
dots.forEach(dot => {
dot.targetBrightness = config.baseBrightness;
});
connections.forEach(conn => {
conn.targetBrightness = config.baseBrightness;
conn.targetThickness = config.baseThickness;
});
// Check which dots are near the mouse
dots.forEach((dot, index) => {
const dx = dot.x - mouseX;
const dy = dot.y - mouseY;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < config.hoverRadius) {
const intensity = 1 - (distance / config.hoverRadius);
dot.targetBrightness = config.baseBrightness + (config.hoverBrightness - config.baseBrightness) * intensity;
// Enhance connected dots and their connections
dot.connections.forEach(connIndex => {
if (dots[connIndex]) {
dots[connIndex].targetBrightness = Math.max(
dots[connIndex].targetBrightness,
config.baseBrightness + (config.hoverBrightness - config.baseBrightness) * intensity * 0.5
);
}
// Find and enhance the connection
const connection = connections.find(
conn => (conn.from === index && conn.to === connIndex) ||
(conn.from === connIndex && conn.to === index)
);
if (connection) {
connection.targetBrightness = Math.max(
connection.targetBrightness,
config.baseBrightness + (config.hoverBrightness - config.baseBrightness) * intensity
);
connection.targetThickness = Math.max(
connection.targetThickness,
config.baseThickness + (config.hoverThickness - config.baseThickness) * intensity
);
}
});
}
});
// Smooth transitions
dots.forEach(dot => {
dot.brightness += (dot.targetBrightness - dot.brightness) * config.fadeSpeed;
});
connections.forEach(conn => {
conn.brightness += (conn.targetBrightness - conn.brightness) * config.fadeSpeed;
conn.thickness += (conn.targetThickness - conn.thickness) * config.fadeSpeed;
});
};
// Animation loop
const animate = () => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const { width, height } = canvas;
// Clear canvas with dark blue background
ctx.fillStyle = '#0a0f1a';
ctx.fillRect(0, 0, width, height);
const dots = dotsRef.current;
const connections = connectionsRef.current;
// Update dot positions
dots.forEach(dot => {
dot.x += dot.vx;
dot.y += dot.vy;
// Bounce off edges
if (dot.x <= 0 || dot.x >= width) {
dot.vx = -dot.vx;
dot.x = Math.max(0, Math.min(width, dot.x));
}
if (dot.y <= 0 || dot.y >= height) {
dot.vy = -dot.vy;
dot.y = Math.max(0, Math.min(height, dot.y));
}
});
// Recalculate connections periodically for smooth movement
if (Math.random() < 0.02) {
connectionsRef.current = calculateConnections(dots);
}
// Update hover effects
updateHoverEffects(dots, connections, mouseRef.current.x, mouseRef.current.y);
// Draw connections with moderate opacity for main background
connections.forEach(conn => {
const fromDot = dots[conn.from];
const toDot = dots[conn.to];
if (!fromDot || !toDot) return;
ctx.strokeStyle = `rgba(100, 200, 255, ${conn.brightness * 0.25})`;
ctx.lineWidth = conn.thickness;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(fromDot.x, fromDot.y);
ctx.lineTo(toDot.x, toDot.y);
ctx.stroke();
});
// Draw dots with moderate opacity for main background
dots.forEach(dot => {
ctx.fillStyle = `rgba(100, 200, 255, ${dot.brightness * 0.25})`;
ctx.beginPath();
ctx.arc(dot.x, dot.y, 2, 0, Math.PI * 2);
ctx.fill();
// Add subtle glow
ctx.shadowColor = `rgba(100, 200, 255, ${dot.brightness * 0.12})`;
ctx.shadowBlur = 4;
ctx.beginPath();
ctx.arc(dot.x, dot.y, 1.5, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
});
animationRef.current = requestAnimationFrame(animate);
};
// Handle resize
const handleResize = () => {
const canvas = canvasRef.current;
if (!canvas) return;
// Set canvas size to window size
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.width = `${window.innerWidth}px`;
canvas.style.height = `${window.innerHeight}px`;
setDimensions({ width: window.innerWidth, height: window.innerHeight });
// Reinitialize dots for new dimensions
dotsRef.current = initializeDots(window.innerWidth, window.innerHeight);
connectionsRef.current = calculateConnections(dotsRef.current);
if (process.env.NODE_ENV === 'development') {
console.log(`MatrixBackground: Canvas resized to ${window.innerWidth}x${window.innerHeight}`);
}
};
// Handle mouse movement
const handleMouseMove = (event: MouseEvent) => {
mouseRef.current = {
x: event.clientX,
y: event.clientY,
};
};
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) {
if (process.env.NODE_ENV === 'development') {
console.log('MatrixBackground: Canvas ref not found');
}
return;
}
if (process.env.NODE_ENV === 'development') {
console.log('MatrixBackground: Canvas found, initializing...');
}
// Initial setup
handleResize();
// Start animation
const startAnimation = () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
animationRef.current = requestAnimationFrame(animate);
};
startAnimation();
// Event listeners
window.addEventListener('resize', handleResize);
document.addEventListener('mousemove', handleMouseMove);
if (process.env.NODE_ENV === 'development') {
console.log('MatrixBackground: Animation started');
}
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
window.removeEventListener('resize', handleResize);
document.removeEventListener('mousemove', handleMouseMove);
if (process.env.NODE_ENV === 'development') {
console.log('MatrixBackground: Cleanup completed');
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<canvas
ref={canvasRef}
className="fixed inset-0"
style={{
zIndex: 0,
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
backgroundColor: '#0a0f1a', // Dark blue background
pointerEvents: 'none'
}}
/>
);
};
export default MatrixBackground;