const nodosData = [ // Reemplaza el src con la ruta real de tus logos { id: 'tl', x0: 220, y0: 140, r: 52, color: '#16a085', html: 'EXCELLENCE' }, { id: 'bl', x0: 200, y0: 420, r: 52, color: '#f39200', html: 'PROJECTS' }, { id: 'tr', x0: 780, y0: 110, r: 52, color: '#c41230', html: 'ACADEMY' }, { id: 'br', x0: 750, y0: 400, r: 52, color: '#0061a0', html: 'DIGITAL' }, { id: 'c', x0: 500, y0: 280, r: 70, color: '#ffffff', html: 'EL TODO' } ]; const conexiones = [ { id: 'arcL', from: 'tl', to: 'bl', type: 'vertical', curveOut: -180 }, { id: 'c1', from: 'tl', to: 'c', type: 'horizontal', curveOut: 0 }, { id: 'c2', from: 'bl', to: 'c', type: 'horizontal', curveOut: 0 }, { id: 'arcR', from: 'tr', to: 'br', type: 'vertical', curveOut: 180 }, { id: 'c3', from: 'tr', to: 'c', type: 'horizontal', curveOut: 0 }, { id: 'c4', from: 'br', to: 'c', type: 'horizontal', curveOut: 0 } ]; const contenedorNodos = document.getElementById('grupo-nodos'); const grupoLineas = document.getElementById('grupo-lineas'); const svgDefs = document.getElementById('svg-defs'); const nodosMap = {}; // 1. Renderizado de Nodos HTML (AHORA FIJOS) nodosData.forEach(nodo => { nodosMap[nodo.id] = nodo; const el = document.createElement('div'); el.className = 'nodo-circulo'; el.id = `node-${nodo.id}`; el.style.backgroundColor = nodo.color; // Lo comento para que el fondo sea blanco y se vea bien tu logo, descomenta si prefieres el fondo de color //el.style.border = `4px solid ${nodo.color}`; // Agrego un borde con el color corporativo el.style.width = `${(nodo.r * 2 / 1000) * 100}%`; el.style.height = `${(nodo.r * 2 / 500) * 100}%`; // Asignamos la posición una única vez nodo.x = nodo.x0; nodo.y = nodo.y0; el.style.left = `${(nodo.x / 1000) * 100}%`; el.style.top = `${(nodo.y / 500) * 100}%`; const content = document.createElement('div'); content.className = 'nodo-contenido'; content.innerHTML = nodo.html; el.appendChild(content); contenedorNodos.appendChild(el); nodo.el = el; }); const offsetsLineas = [-12, -6, 0, 6, 12]; // 2. Pre-calcular rutas y gradientes (UNA SOLA VEZ) conexiones.forEach(conn => { const n1 = nodosMap[conn.from]; const n2 = nodosMap[conn.to]; const isHoriz = conn.type === 'horizontal'; conn.rutasCache = {}; // Diccionario para guardar las rutas // Crear y fijar Gradiente const gradId = `grad-${conn.from}-${conn.to}`; const grad = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient'); grad.setAttribute('id', gradId); grad.setAttribute('gradientUnits', 'userSpaceOnUse'); grad.setAttribute('x1', n1.x); grad.setAttribute('y1', n1.y); grad.setAttribute('x2', n2.x); grad.setAttribute('y2', n2.y); const stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop'); stop1.setAttribute('offset', '0%'); stop1.setAttribute('stop-color', n1.color); const stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop'); stop2.setAttribute('offset', '100%'); const colorTarget = n2.id === 'c' ? '#0e626e' : n2.color; stop2.setAttribute('stop-color', colorTarget); grad.appendChild(stop1); grad.appendChild(stop2); svgDefs.appendChild(grad); // Función matemática const calcularRuta = (offsetX, offsetY) => { let cp1x, cp1y, cp2x, cp2y; if (isHoriz) { const distanciaX = n2.x - n1.x; cp1x = n1.x + distanciaX * 0.5; cp1y = n1.y; cp2x = n2.x - distanciaX * 0.5; cp2y = n2.y; } else { cp1x = n1.x + conn.curveOut; cp1y = n1.y; cp2x = n2.x + conn.curveOut; cp2y = n2.y; } return `M ${n1.x + offsetX} ${n1.y + offsetY} C ${cp1x + offsetX} ${cp1y + offsetY}, ${cp2x + offsetX} ${cp2y + offsetY}, ${n2.x + offsetX} ${n2.y + offsetY}`; }; // Dibujar y Cachear las 5 líneas base conn.pathsBase = []; offsetsLineas.forEach(offset => { const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.classList.add('linea-base'); path.setAttribute('stroke', `url(#${gradId})`); const rutaCalculada = calcularRuta(isHoriz ? 0 : offset, isHoriz ? offset : 0); path.setAttribute('d', rutaCalculada); conn.rutasCache[offset] = rutaCalculada; // Guardado en caché grupoLineas.appendChild(path); conn.pathsBase.push({ el: path, offset: offset }); }); // Crear pulsos conn.pulsos = []; const cantPulsos = 6; for (let i = 0; i < cantPulsos; i++) { const pathPulso = document.createElementNS('http://www.w3.org/2000/svg', 'path'); pathPulso.classList.add('linea-pulso'); const carrilInicial = offsetsLineas[Math.floor(Math.random() * offsetsLineas.length)]; pathPulso.setAttribute('d', conn.rutasCache[carrilInicial]); // Le asignamos su ruta desde la caché grupoLineas.appendChild(pathPulso); conn.pulsos.push({ el: pathPulso, currentOffset: carrilInicial, needsPathUpdate: false, // Bandera de optimización dash: (1200 / cantPulsos) * i + Math.random() * 50, speed: 2.0 + Math.random() * 1.5 }); } }); // 3. Lógica de salto entre carriles setInterval(() => { conexiones.forEach(conn => { conn.pulsos.forEach(p => { if (Math.random() > 0.8) { p.currentOffset = offsetsLineas[Math.floor(Math.random() * offsetsLineas.length)]; p.needsPathUpdate = true; // Avisamos que en el próximo frame debe cambiar de ruta } }); }); }, 1200); // 4. Motor de Animación Ultraligero let animando = false; let rafId = null; function animarFrame() { if (!animando) return; // corta el loop cuando no es visible conexiones.forEach(conn => { conn.pulsos.forEach(p => { p.dash -= p.speed; if (p.needsPathUpdate) { p.el.setAttribute('d', conn.rutasCache[p.currentOffset]); p.needsPathUpdate = false; } p.el.style.strokeDashoffset = p.dash; }); }); rafId = requestAnimationFrame(animarFrame); } function iniciarAnimacion() { if (animando) return; animando = true; rafId = requestAnimationFrame(animarFrame); } function detenerAnimacion() { animando = false; if (rafId) cancelAnimationFrame(rafId); } // Detectar visibilidad de la sección del header const heroVisual = document.querySelector('.hero-visual'); if (heroVisual && 'IntersectionObserver' in window) { const io = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) iniciarAnimacion(); else detenerAnimacion(); }); }, { threshold: 0.05 }); io.observe(heroVisual); } else { iniciarAnimacion(); // fallback } // Pausar también cuando la pestaña queda oculta document.addEventListener('visibilitychange', () => { if (document.hidden) detenerAnimacion(); else if (heroVisual && heroVisual.getBoundingClientRect().top < window.innerHeight) { iniciarAnimacion(); } });