class MatrixBackground { constructor() { this.canvas = document.getElementById('matrixCanvas'); if (!this.canvas) return; this.ctx = this.canvas.getContext('2d'); if (!this.ctx) return; this.dots = []; this.connections = []; this.mouse = { x: -9999, y: -9999, active: false }; this.rafId = null; this.isRunning = false; this.lastTimestamp = 0; this.motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); this.prefersReducedMotion = this.motionQuery.matches; this.dotSpriteCache = new Map(); this.onAnimationFrame = (timestamp) => this.animate(timestamp); this.config = { dotCount: this.getMobileDotCount(), dotSpeed: 0.2, connectionDuration: { min: 2000, max: 6000 }, connectionChance: 0.0002, maxConnections: this.getMobileMaxConnections(), dotSize: 3, mouseRadius: 110, mouseForce: 0.6, colors: { dotDefault: 'rgba(230, 240, 255, 0.78)', dotDefaultGlow: 'rgba(180, 220, 255, 0.40)', dotConnected: 'rgba(120, 190, 245, 0.85)', dotConnectedGlow: 'rgba(79, 195, 247, 0.55)', connection: { start: 'rgba(102, 126, 234, 0.22)', middle: 'rgba(118, 75, 162, 0.62)', end: 'rgba(79, 195, 247, 0.22)' } } }; this.init(); } getMobileDotCount() { if (window.innerWidth <= 480) return 35; if (window.innerWidth <= 768) return 45; if (window.innerWidth >= 2100) return 100; return 50; } getMobileMaxConnections() { if (window.innerWidth <= 480) return 4; if (window.innerWidth <= 768) return 6; return 8; } init() { this.handleResize(); this.bindEvents(); if (this.prefersReducedMotion) { this.renderFrame(0); } else { this.start(); } } bindEvents() { window.addEventListener('pointermove', (event) => { this.mouse.x = event.clientX; this.mouse.y = event.clientY; this.mouse.active = true; }, { passive: true }); window.addEventListener('pointerout', () => { this.mouse.active = false; this.mouse.x = -9999; this.mouse.y = -9999; }); document.addEventListener('visibilitychange', () => { if (document.hidden) { this.stop(); } else if (this.prefersReducedMotion) { this.renderFrame(this.lastTimestamp); } else { this.start(); } }); const updateMotionPreference = (event) => { this.prefersReducedMotion = event.matches; if (this.prefersReducedMotion) { this.stop(); this.renderFrame(this.lastTimestamp); } else if (!document.hidden) { this.start(); } }; if (this.motionQuery.addEventListener) { this.motionQuery.addEventListener('change', updateMotionPreference); } else { this.motionQuery.addListener(updateMotionPreference); } if (window.innerWidth > 768) { let resizeRaf = null; window.addEventListener('resize', () => { if (resizeRaf !== null) cancelAnimationFrame(resizeRaf); resizeRaf = requestAnimationFrame(() => { resizeRaf = null; this.handleResize(); }); }); } } start() { if (this.rafId !== null || this.prefersReducedMotion || document.hidden) return; this.isRunning = true; this.rafId = requestAnimationFrame(this.onAnimationFrame); } stop() { if (this.rafId !== null) { cancelAnimationFrame(this.rafId); } this.rafId = null; this.isRunning = false; } handleResize() { this.canvas.width = window.innerWidth; this.canvas.height = window.innerHeight; this.config.dotCount = this.getMobileDotCount(); this.config.maxConnections = this.getMobileMaxConnections(); this.connections = []; this.createDots(); if (this.prefersReducedMotion) { this.renderFrame(this.lastTimestamp); } } createDots() { this.dots = []; for (let index = 0; index < this.config.dotCount; index++) { this.createDot(); } } createDot() { const startX = Math.random() * (this.canvas.width - 20) + 10; const startY = Math.random() * (this.canvas.height - 20) + 10; const angle = Math.random() * Math.PI * 2; const speed = this.config.dotSpeed * (0.8 + Math.random() * 0.4); const baseVelocityX = Math.cos(angle) * speed; const baseVelocityY = Math.sin(angle) * speed; this.dots.push({ x: startX, y: startY, vx: baseVelocityX, vy: baseVelocityY, baseVx: baseVelocityX, baseVy: baseVelocityY, wanderPhase: Math.random() * Math.PI * 2, wanderSpeed: 0.01 + Math.random() * 0.01, opacity: Math.random() * 0.3 + 0.7, size: this.config.dotSize + Math.random() * 2, connectionCount: 0 }); } updateDots() { const radius = this.config.mouseRadius; const radiusSquared = radius * radius; const force = this.config.mouseForce; for (const dot of this.dots) { if (this.mouse.active) { const dx = dot.x - this.mouse.x; const dy = dot.y - this.mouse.y; const distanceSquared = dx * dx + dy * dy; if (distanceSquared < radiusSquared && distanceSquared > 0.5) { const distance = Math.sqrt(distanceSquared); const strength = (1 - distance / radius) * force; dot.vx += (dx / distance) * strength; dot.vy += (dy / distance) * strength; } } dot.x += dot.vx; dot.y += dot.vy; dot.wanderPhase += dot.wanderSpeed; const wanderX = Math.cos(dot.wanderPhase) * 0.03; const wanderY = Math.sin(dot.wanderPhase * 0.8) * 0.03; dot.vx += (dot.baseVx + wanderX - dot.vx) * 0.02; dot.vy += (dot.baseVy + wanderY - dot.vy) * 0.02; const dotRadius = dot.size / 2; const minX = dotRadius; const minY = dotRadius; const maxX = Math.max(minX, this.canvas.width - dotRadius); const maxY = Math.max(minY, this.canvas.height - dotRadius); if (dot.x <= minX && dot.vx < 0) { dot.x = minX + (minX - dot.x); dot.vx = Math.abs(dot.vx); dot.baseVx = Math.abs(dot.baseVx); } else if (dot.x >= maxX && dot.vx > 0) { dot.x = maxX - (dot.x - maxX); dot.vx = -Math.abs(dot.vx); dot.baseVx = -Math.abs(dot.baseVx); } if (dot.y <= minY && dot.vy < 0) { dot.y = minY + (minY - dot.y); dot.vy = Math.abs(dot.vy); dot.baseVy = Math.abs(dot.baseVy); } else if (dot.y >= maxY && dot.vy > 0) { dot.y = maxY - (dot.y - maxY); dot.vy = -Math.abs(dot.vy); dot.baseVy = -Math.abs(dot.baseVy); } dot.x = Math.max(minX, Math.min(maxX, dot.x)); dot.y = Math.max(minY, Math.min(maxY, dot.y)); } } getDotSprite(size, isConnected) { const roundedSize = Math.round(size * 2) / 2; const state = isConnected ? 'connected' : 'default'; const cacheKey = state + ':' + roundedSize; const cachedSprite = this.dotSpriteCache.get(cacheKey); if (cachedSprite) { return cachedSprite; } const padding = 10; const dimension = Math.ceil(roundedSize + padding * 2); const sprite = document.createElement('canvas'); sprite.width = dimension; sprite.height = dimension; const spriteContext = sprite.getContext('2d'); spriteContext.shadowBlur = 6; spriteContext.shadowColor = isConnected ? this.config.colors.dotConnectedGlow : this.config.colors.dotDefaultGlow; spriteContext.fillStyle = isConnected ? this.config.colors.dotConnected : this.config.colors.dotDefault; spriteContext.beginPath(); spriteContext.arc(dimension / 2, dimension / 2, roundedSize / 2, 0, Math.PI * 2); spriteContext.fill(); this.dotSpriteCache.set(cacheKey, sprite); return sprite; } drawDots() { for (const dot of this.dots) { const sprite = this.getDotSprite(dot.size, dot.connectionCount > 0); const halfDimension = sprite.width / 2; this.ctx.globalAlpha = dot.opacity; this.ctx.drawImage(sprite, dot.x - halfDimension, dot.y - halfDimension); } this.ctx.globalAlpha = 1; } createConnection(dot1, dot2, timestamp) { if (this.connections.length >= this.config.maxConnections) return; dot1.connectionCount++; dot2.connectionCount++; this.connections.push({ dot1, dot2, startTime: timestamp, duration: Math.random() * (this.config.connectionDuration.max - this.config.connectionDuration.min) + this.config.connectionDuration.min }); } updateConnections(timestamp) { let writeIndex = 0; for (let readIndex = 0; readIndex < this.connections.length; readIndex++) { const connection = this.connections[readIndex]; if (timestamp - connection.startTime > connection.duration) { connection.dot1.connectionCount--; connection.dot2.connectionCount--; continue; } this.connections[writeIndex] = connection; writeIndex++; } this.connections.length = writeIndex; } samplePoisson(lambda) { if (lambda <= 0) return 0; const threshold = Math.exp(-lambda); let product = 1; let count = 0; do { count++; product *= Math.random(); } while (product > threshold); return count - 1; } tryCreateConnections(timestamp) { const dotCount = this.dots.length; if (dotCount < 2 || this.connections.length >= this.config.maxConnections) return; const pairCount = dotCount * (dotCount - 1) / 2; const attempts = this.samplePoisson(pairCount * this.config.connectionChance); for (let attempt = 0; attempt < attempts && this.connections.length < this.config.maxConnections; attempt++) { const firstIndex = Math.floor(Math.random() * dotCount); let secondIndex = Math.floor(Math.random() * (dotCount - 1)); if (secondIndex >= firstIndex) { secondIndex++; } const dot1 = this.dots[firstIndex]; const dot2 = this.dots[secondIndex]; const dx = dot2.x - dot1.x; const dy = dot2.y - dot1.y; const distanceSquared = dx * dx + dy * dy; if (distanceSquared >= 22500 || distanceSquared <= 900) { continue; } const alreadyConnected = this.connections.some((connection) => (connection.dot1 === dot1 && connection.dot2 === dot2) || (connection.dot1 === dot2 && connection.dot2 === dot1) ); if (!alreadyConnected) { this.createConnection(dot1, dot2, timestamp); } } } drawConnections(timestamp) { for (const connection of this.connections) { const elapsed = timestamp - connection.startTime; const opacity = Math.min(1, elapsed / 500); const gradient = this.ctx.createLinearGradient( connection.dot1.x, connection.dot1.y, connection.dot2.x, connection.dot2.y ); gradient.addColorStop(0, this.config.colors.connection.start); gradient.addColorStop(0.5, this.config.colors.connection.middle); gradient.addColorStop(1, this.config.colors.connection.end); this.ctx.beginPath(); this.ctx.moveTo(connection.dot1.x, connection.dot1.y); this.ctx.lineTo(connection.dot2.x, connection.dot2.y); this.ctx.strokeStyle = gradient; this.ctx.lineWidth = 2; this.ctx.globalAlpha = opacity; const pulse = (Math.sin((elapsed / 2000) * Math.PI * 2) + 1) / 2; this.ctx.shadowBlur = 4 + pulse * 6; this.ctx.shadowColor = 'rgba(118, 75, 162, 0.5)'; this.ctx.stroke(); } this.ctx.globalAlpha = 1; this.ctx.shadowBlur = 0; } renderFrame(timestamp) { this.lastTimestamp = timestamp; this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); if (!this.prefersReducedMotion) { this.updateDots(); this.updateConnections(timestamp); this.tryCreateConnections(timestamp); } this.drawConnections(timestamp); this.drawDots(); } animate(timestamp) { this.rafId = null; this.renderFrame(timestamp); if (this.isRunning && !this.prefersReducedMotion && !document.hidden) { this.rafId = requestAnimationFrame(this.onAnimationFrame); } } } document.addEventListener('DOMContentLoaded', () => { new MatrixBackground(); });