/** * Hexagonal Magnetism Background - OPTIMIZED * Only renders visible hexagons, uses viewport culling */ (function() { 'use strict'; // Configuration const CONFIG = { hexSize: 30, lineWidth: 1, maxLineWidth: 2.5, baseOpacity: 0.15, maxOpacity: 0.6, magnetRadius: 280, maxDisplacement: 12, returnSpeed: 0.4, // Pre-calculated colors baseR: 119, baseG: 119, baseB: 100, tealR: 38, tealG: 166, tealB: 154, orangeR: 245, orangeG: 124, orangeB: 0 }; // Pre-calculate hex geometry const SQRT3 = Math.sqrt(3); const horizSpacing = CONFIG.hexSize * SQRT3; const vertSpacing = CONFIG.hexSize * 1.5; // Pre-calculate hex vertices angles (pointy-top) const HEX_ANGLES = []; for (let i = 0; i < 6; i++) { const angle = (Math.PI / 3) * i - Math.PI / 2; HEX_ANGLES.push({ cos: Math.cos(angle), sin: Math.sin(angle) }); } // State let canvas, ctx; let mouse = { x: -1000, y: -1000 }; let scrollY = 0; let animationId; let isInitialized = false; let canvasWidth = 0; let canvasHeight = 0; // Active hexagons (only those being animated) let activeHexagons = new Map(); // Easing function function easeOutCubic(t) { return 1 - (1 - t) * (1 - t) * (1 - t); } // Get hex key from grid position function getHexKey(col, row) { return `${col},${row}`; } // Get hex origin from grid position function getHexOrigin(col, row) { const xOffset = (row % 2 === 1) ? horizSpacing / 2 : 0; return { x: col * horizSpacing + xOffset, y: row * vertSpacing }; } // Draw a single hexagon function drawHex(x, y, opacity, lineWidth, colorInfluence) { ctx.beginPath(); ctx.moveTo( x + CONFIG.hexSize * HEX_ANGLES[0].cos, y + CONFIG.hexSize * HEX_ANGLES[0].sin ); for (let i = 1; i < 6; i++) { ctx.lineTo( x + CONFIG.hexSize * HEX_ANGLES[i].cos, y + CONFIG.hexSize * HEX_ANGLES[i].sin ); } ctx.closePath(); // Color calculation let r, g, b; if (colorInfluence < 0.01) { r = CONFIG.baseR; g = CONFIG.baseG; b = CONFIG.baseB; } else if (colorInfluence < 0.6) { const t = colorInfluence / 0.6; r = CONFIG.baseR + (CONFIG.tealR - CONFIG.baseR) * t | 0; g = CONFIG.baseG + (CONFIG.tealG - CONFIG.baseG) * t | 0; b = CONFIG.baseB + (CONFIG.tealB - CONFIG.baseB) * t | 0; } else { const t = (colorInfluence - 0.6) / 0.4; r = CONFIG.tealR + (CONFIG.orangeR - CONFIG.tealR) * t | 0; g = CONFIG.tealG + (CONFIG.orangeG - CONFIG.tealG) * t | 0; b = CONFIG.tealB + (CONFIG.orangeB - CONFIG.tealB) * t | 0; } ctx.strokeStyle = `rgba(${r},${g},${b},${opacity})`; ctx.lineWidth = lineWidth; ctx.stroke(); } // Initialize function init() { canvas = document.getElementById('hexCanvas'); if (!canvas) return; ctx = canvas.getContext('2d', { alpha: true }); resize(); window.addEventListener('resize', debounce(resize, 200)); document.addEventListener('mousemove', handleMouseMove, { passive: true }); document.addEventListener('mouseleave', handleMouseLeave); window.addEventListener('scroll', handleScroll, { passive: true }); isInitialized = true; animate(); } function resize() { const dpr = Math.min(window.devicePixelRatio || 1, 2); // Cap DPR at 2 canvasWidth = window.innerWidth; canvasHeight = window.innerHeight; canvas.width = canvasWidth * dpr; canvas.height = canvasHeight * dpr; canvas.style.width = canvasWidth + 'px'; canvas.style.height = canvasHeight + 'px'; canvas.style.position = 'fixed'; canvas.style.top = '0'; canvas.style.left = '0'; ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.scale(dpr, dpr); } function handleMouseMove(e) { mouse.x = e.clientX; mouse.y = e.clientY; } function handleMouseLeave() { mouse.x = -1000; mouse.y = -1000; } function handleScroll() { scrollY = window.scrollY; } // Main animation loop - optimized function animate() { if (!isInitialized) return; ctx.clearRect(0, 0, canvasWidth, canvasHeight); // Calculate visible grid range (viewport only) const viewTop = scrollY - CONFIG.hexSize; const viewBottom = scrollY + canvasHeight + CONFIG.hexSize; const viewLeft = -CONFIG.hexSize; const viewRight = canvasWidth + CONFIG.hexSize; const startRow = Math.floor(viewTop / vertSpacing) - 1; const endRow = Math.ceil(viewBottom / vertSpacing) + 1; const startCol = Math.floor(viewLeft / horizSpacing) - 1; const endCol = Math.ceil(viewRight / horizSpacing) + 1; // Mouse position in world coordinates const mouseWorldY = mouse.y + scrollY; const magnetRadiusSq = CONFIG.magnetRadius * CONFIG.magnetRadius; // Update active hexagons for (const [key, hex] of activeHexagons) { const dx = mouse.x - hex.originX; const dy = mouseWorldY - hex.originY; const distSq = dx * dx + dy * dy; if (distSq < magnetRadiusSq && mouse.x > -500) { const distance = Math.sqrt(distSq); const influence = 1 - (distance / CONFIG.magnetRadius); const easedInfluence = easeOutCubic(influence); const angle = Math.atan2(dy, dx); const displacement = CONFIG.maxDisplacement * easedInfluence; hex.currentX += (hex.originX + Math.cos(angle) * displacement - hex.currentX) * 0.15; hex.currentY += (hex.originY + Math.sin(angle) * displacement - hex.currentY) * 0.15; hex.opacity = CONFIG.baseOpacity + (CONFIG.maxOpacity - CONFIG.baseOpacity) * easedInfluence; hex.lineWidth = CONFIG.lineWidth + (CONFIG.maxLineWidth - CONFIG.lineWidth) * easedInfluence; hex.colorInfluence = easedInfluence; } else { // Return to origin hex.currentX += (hex.originX - hex.currentX) * CONFIG.returnSpeed; hex.currentY += (hex.originY - hex.currentY) * CONFIG.returnSpeed; hex.opacity += (CONFIG.baseOpacity - hex.opacity) * CONFIG.returnSpeed; hex.lineWidth += (CONFIG.lineWidth - hex.lineWidth) * CONFIG.returnSpeed; hex.colorInfluence *= (1 - CONFIG.returnSpeed); // Remove if fully reset if (hex.colorInfluence < 0.001 && Math.abs(hex.currentX - hex.originX) < 0.1 && Math.abs(hex.currentY - hex.originY) < 0.1) { activeHexagons.delete(key); } } } // Draw visible hexagons for (let row = startRow; row <= endRow; row++) { for (let col = startCol; col <= endCol; col++) { const origin = getHexOrigin(col, row); const screenY = origin.y - scrollY; // Skip if outside viewport if (screenY < -CONFIG.hexSize || screenY > canvasHeight + CONFIG.hexSize) continue; const key = getHexKey(col, row); const activeHex = activeHexagons.get(key); if (activeHex) { // Draw animated hex drawHex( activeHex.currentX, activeHex.currentY - scrollY, activeHex.opacity, activeHex.lineWidth, activeHex.colorInfluence ); } else { // Check if should become active const dx = mouse.x - origin.x; const dy = mouseWorldY - origin.y; const distSq = dx * dx + dy * dy; if (distSq < magnetRadiusSq && mouse.x > -500) { // Add to active hexagons activeHexagons.set(key, { originX: origin.x, originY: origin.y, currentX: origin.x, currentY: origin.y, opacity: CONFIG.baseOpacity, lineWidth: CONFIG.lineWidth, colorInfluence: 0 }); } // Draw static hex drawHex(origin.x, screenY, CONFIG.baseOpacity, CONFIG.lineWidth, 0); } } } animationId = requestAnimationFrame(animate); } function debounce(func, wait) { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => func(...args), wait); }; } function destroy() { if (animationId) cancelAnimationFrame(animationId); isInitialized = false; activeHexagons.clear(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } window.HexBackground = { init, destroy, config: CONFIG }; })();