import React, { useEffect, useRef } from 'react'; interface Point { x: number; y: number; } interface ShapeDefinition { points: Point[]; type: 'doc' | 'brain' | 'code' | 'graph'; } interface DocumentEntity { id: number; gridX: number; gridY: number; life: number; state: 'forming' | 'flipping' | 'fading'; flipProgress: number; opacity: number; shape: ShapeDefinition; } const Background: React.FC<{ theme: 'light' | 'dark' }> = ({ theme }) => { const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; let animationFrameId: number; let mouseX = -1000; let mouseY = -1000; // Grid configuration const DOT_SPACING = 25; // Denser grid const DOT_RADIUS = 1.2; // Shape Definitions const DOC_SHAPE: ShapeDefinition = { type: 'doc', points: [ { x: 0, y: 0 }, { x: 1, y: 0 }, { x: 2, y: 0 }, /* corner fold */ { x: 4, y: 1 }, { x: 0, y: 1 }, { x: 0, y: 2 }, { x: 0, y: 3 }, { x: 0, y: 4 }, { x: 0, y: 5 }, { x: 4, y: 2 }, { x: 4, y: 3 }, { x: 4, y: 4 }, { x: 4, y: 5 }, { x: 1, y: 5 }, { x: 2, y: 5 }, { x: 3, y: 5 }, { x: 3, y: 0 }, { x: 4, y: 1 }, { x: 1, y: 2 }, { x: 2, y: 2 }, { x: 3, y: 2 }, { x: 1, y: 4 }, { x: 2, y: 4 }, { x: 3, y: 4 }, ] }; const CODE_SHAPE: ShapeDefinition = { type: 'code', points: [ // < { x: 2, y: 0 }, { x: 1, y: 1 }, { x: 0, y: 2 }, { x: 1, y: 3 }, { x: 2, y: 4 }, // / { x: 3, y: 0 }, { x: 3, y: 4 }, // Slash approximation // > { x: 4, y: 0 }, { x: 5, y: 1 }, { x: 6, y: 2 }, { x: 5, y: 3 }, { x: 4, y: 4 }, ] }; const BRAIN_SHAPE: ShapeDefinition = { type: 'brain', points: [ { x: 2, y: 0 }, { x: 3, y: 0 }, { x: 1, y: 1 }, { x: 4, y: 1 }, { x: 0, y: 2 }, { x: 5, y: 2 }, { x: 0, y: 3 }, { x: 5, y: 3 }, { x: 1, y: 4 }, { x: 4, y: 4 }, { x: 2, y: 5 }, { x: 3, y: 5 }, // Internal connections { x: 2, y: 2 }, { x: 3, y: 2 }, { x: 2, y: 3 }, { x: 3, y: 3 }, ] }; const GRAPH_SHAPE: ShapeDefinition = { type: 'graph', points: [ { x: 0, y: 4 }, { x: 1, y: 3 }, { x: 2, y: 4 }, { x: 3, y: 2 }, { x: 4, y: 3 }, { x: 5, y: 0 }, // Nodes { x: 0, y: 4 }, { x: 2, y: 4 }, { x: 3, y: 2 }, { x: 5, y: 0 } ] }; const SHAPES = [DOC_SHAPE, CODE_SHAPE, BRAIN_SHAPE, GRAPH_SHAPE]; let documents: DocumentEntity[] = []; let nextDocId = 0; let lastSpawnTime = 0; let pulseTime = 0; const resize = () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; }; window.addEventListener('resize', resize); resize(); const handleMouseMove = (e: MouseEvent) => { mouseX = e.clientX; mouseY = e.clientY; }; window.addEventListener('mousemove', handleMouseMove); const spawnDocument = () => { const cols = Math.ceil(canvas.width / DOT_SPACING); const rows = Math.ceil(canvas.height / DOT_SPACING); // Spawn shapes only in the edge regions (left 15% or right 15%) // to avoid cluttering the content area const leftZoneCols = Math.floor(cols * 0.12); const rightZoneStart = Math.floor(cols * 0.88); // Randomly pick left or right edge zone const useLeftZone = Math.random() < 0.5; const gridX = useLeftZone ? Math.floor(Math.random() * Math.max(1, leftZoneCols - 8)) : rightZoneStart + Math.floor(Math.random() * Math.max(1, cols - rightZoneStart - 8)); const gridY = Math.floor(Math.random() * (rows - 8)); const shape = SHAPES[Math.floor(Math.random() * SHAPES.length)]; documents.push({ id: nextDocId++, gridX, gridY, life: 0, state: 'forming', flipProgress: 0, opacity: 0, shape }); }; const draw = (timestamp: number) => { ctx.clearRect(0, 0, canvas.width, canvas.height); const cols = Math.ceil(canvas.width / DOT_SPACING); const rows = Math.ceil(canvas.height / DOT_SPACING); const baseDotColor = theme === 'dark' ? 'rgba(255, 255, 255, 0.15)' : 'rgba(0, 0, 0, 0.15)'; const activeDotColor = theme === 'dark' ? 'rgba(99, 102, 241, 0.9)' : 'rgba(79, 70, 229, 0.9)'; // Pulse wave pulseTime += 0.05; // Define content-safe zone (center area where text is displayed) // Dots will fade out as they approach this zone const contentLeft = canvas.width * 0.15; // Left 15% is safe for dots const contentRight = canvas.width * 0.85; // Right 15% is safe for dots const contentTop = canvas.height * 0.08; // Top 8% is safe for dots const fadeWidth = 100; // Pixels over which dots fade out // Draw background grid with interaction ctx.fillStyle = baseDotColor; for (let i = 0; i < cols; i++) { for (let j = 0; j < rows; j++) { const x = i * DOT_SPACING + (DOT_SPACING / 2); const y = j * DOT_SPACING + (DOT_SPACING / 2); // Calculate opacity based on distance from content zone let zoneOpacity = 1; // Fade out dots as they enter the content area if (x > contentLeft && x < contentRight && y > contentTop) { // Inside content zone - calculate fade based on distance from edges const distFromLeft = x - contentLeft; const distFromRight = contentRight - x; const distFromTop = y - contentTop; const minDistX = Math.min(distFromLeft, distFromRight); const minDist = Math.min(minDistX, distFromTop); // Fade out completely within fadeWidth pixels of entering content zone zoneOpacity = Math.max(0, 1 - (minDist / fadeWidth)); // Skip drawing dots that are fully transparent if (zoneOpacity < 0.01) continue; } // Mouse interaction const dx = x - mouseX; const dy = y - mouseY; const dist = Math.sqrt(dx * dx + dy * dy); let scale = 1; let alpha = 1; // Mouse ripple if (dist < 150) { scale = 1 + (150 - dist) / 150; // Scale up to 2x alpha = 1 + (150 - dist) / 100; } // Global pulse wave const wave = Math.sin(x * 0.01 + y * 0.01 + pulseTime) * 0.5 + 0.5; if (wave > 0.8) { scale += 0.2; alpha += 0.2; } ctx.globalAlpha = Math.min(alpha, 0.5) * zoneOpacity; // Apply zone fade ctx.beginPath(); ctx.arc(x, y, DOT_RADIUS * scale, 0, Math.PI * 2); ctx.fill(); } } ctx.globalAlpha = 1; // Update and draw entities if (timestamp - lastSpawnTime > 1500) { // Slightly faster spawn spawnDocument(); lastSpawnTime = timestamp; } documents = documents.filter(doc => doc.state !== 'fading' || doc.opacity > 0.01); documents.forEach(doc => { if (doc.state === 'forming') { doc.opacity += 0.03; if (doc.opacity >= 1) { doc.opacity = 1; doc.state = 'flipping'; } } else if (doc.state === 'flipping') { doc.flipProgress += 0.03; if (doc.flipProgress >= Math.PI) { doc.state = 'fading'; } } else if (doc.state === 'fading') { doc.opacity -= 0.03; } const centerX = doc.gridX + 3; // Approx center ctx.fillStyle = activeDotColor; doc.shape.points.forEach(pt => { const gx = doc.gridX + pt.x; const gy = doc.gridY + pt.y; let screenX = gx * DOT_SPACING + (DOT_SPACING / 2); const screenY = gy * DOT_SPACING + (DOT_SPACING / 2); if (doc.state === 'flipping' || doc.state === 'fading') { const docCenterScreenX = centerX * DOT_SPACING + (DOT_SPACING / 2); const relativeX = screenX - docCenterScreenX; const scaleX = Math.cos(doc.flipProgress); screenX = docCenterScreenX + (relativeX * scaleX); } ctx.globalAlpha = doc.opacity; ctx.beginPath(); ctx.arc(screenX, screenY, DOT_RADIUS * 2.8, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = 1.0; }); }); animationFrameId = requestAnimationFrame(draw); }; animationFrameId = requestAnimationFrame(draw); return () => { window.removeEventListener('resize', resize); window.removeEventListener('mousemove', handleMouseMove); cancelAnimationFrame(animationFrameId); }; }, [theme]); return ( ); }; export default Background;