// Particle Network class ParticleNetwork { constructor(colorConfig = null) { this.canvas = document.getElementById('particleCanvas'); if (!this.canvas) { console.error('ParticleNetwork: Canvas element not found'); return; } this.ctx = this.canvas.getContext('2d'); this.particles = []; this.mouse = { x: -1000, y: -1000 }; // Start off-screen this.particleCount = 80; this.mouseRadius = 200; // Radio de influencia del mouse para conexiones // Color configuration - default to cyan/green this.colors = colorConfig || { particle: 'rgba(0, 166, 255, 0.6)', line: 'rgba(0, 145, 255, 0.36)' }; this.init(); } init() { this.resizeCanvas(); this.createParticles(); this.setupEventListeners(); this.animate(); } resizeCanvas() { this.canvas.width = window.innerWidth; this.canvas.height = window.innerHeight; } createParticles() { this.particles = []; for (let i = 0; i < this.particleCount; i++) { this.particles.push({ x: Math.random() * this.canvas.width, y: Math.random() * this.canvas.height, vx: (Math.random() - 0.5) * 0.5, vy: (Math.random() - 0.5) * 0.5, radius: Math.random() * 2 + 1 }); } } setupEventListeners() { window.addEventListener('resize', () => { this.resizeCanvas(); this.createParticles(); }); window.addEventListener('mousemove', (e) => { this.mouse.x = e.clientX; this.mouse.y = e.clientY; }); // Reset mouse position when it leaves the window window.addEventListener('mouseleave', () => { this.mouse.x = -1000; this.mouse.y = -1000; }); } drawParticle(particle) { this.ctx.beginPath(); this.ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2); this.ctx.fillStyle = this.colors.particle; this.ctx.fill(); } drawLine(p1, p2, opacity) { this.ctx.beginPath(); this.ctx.moveTo(p1.x, p1.y); this.ctx.lineTo(p2.x, p2.y); // Extract RGB from the line color and apply opacity const baseColor = this.colors.line.match(/rgba?\(([^)]+)\)/)[1].split(',').slice(0, 3).join(','); this.ctx.strokeStyle = `rgba(${baseColor}, ${opacity})`; this.ctx.lineWidth = 1; this.ctx.stroke(); } updateParticle(particle) { // Update position particle.x += particle.vx; particle.y += particle.vy; // Bounce off edges if (particle.x < 0 || particle.x > this.canvas.width) { particle.vx *= -1; } if (particle.y < 0 || particle.y > this.canvas.height) { particle.vy *= -1; } // Mouse interaction removed - particles just float naturally } animate() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // Update and draw particles this.particles.forEach((particle, i) => { this.updateParticle(particle); this.drawParticle(particle); // Connect particles ONLY if mouse is near them this.particles.slice(i + 1).forEach(otherParticle => { const dx = particle.x - otherParticle.x; const dy = particle.y - otherParticle.y; const distance = Math.sqrt(dx * dx + dy * dy); // Check if particles are close to each other if (distance < 150) { // Check if mouse is near either particle const mouseToP1 = Math.sqrt( Math.pow(this.mouse.x - particle.x, 2) + Math.pow(this.mouse.y - particle.y, 2) ); const mouseToP2 = Math.sqrt( Math.pow(this.mouse.x - otherParticle.x, 2) + Math.pow(this.mouse.y - otherParticle.y, 2) ); // Only draw line if mouse is within radius of either particle if (mouseToP1 < this.mouseRadius || mouseToP2 < this.mouseRadius) { // Calculate opacity based on distance and mouse proximity const proximityFactor = Math.min(mouseToP1, mouseToP2) / this.mouseRadius; const distanceFactor = 1 - (distance / 150); const opacity = 0.3 * distanceFactor * (1 - proximityFactor); this.drawLine(particle, otherParticle, opacity); } } }); }); requestAnimationFrame(() => this.animate()); } } // Animated Text Rotator class TextRotator { constructor(containerSelector, interval = 3000) { this.container = document.querySelector(containerSelector); if (!this.container) { console.warn('TextRotator: Container not found'); return; } this.texts = this.container.querySelectorAll('.animated-text'); if (this.texts.length === 0) { console.warn('TextRotator: No animated text elements found'); return; } this.currentIndex = 0; this.interval = interval; this.start(); } start() { setInterval(() => { this.texts[this.currentIndex].classList.remove('active'); this.currentIndex = (this.currentIndex + 1) % this.texts.length; this.texts[this.currentIndex].classList.add('active'); }, this.interval); } } // Smooth Scroll function smoothScroll(target) { const element = document.querySelector(target); if (element) { const headerOffset = 64; const elementPosition = element.getBoundingClientRect().top; const offsetPosition = elementPosition + window.pageYOffset - headerOffset; window.scrollTo({ top: offsetPosition, behavior: 'smooth' }); } } // Navigation function setupNavigation() { const mobileMenuBtn = document.getElementById('mobileMenuBtn'); const navLinksContainer = document.getElementById('navLinks'); if (!mobileMenuBtn || !navLinksContainer) return; // Para móvil: el enlace "Divisiones" SOLO abre el submenú, NO navega const divisionesNavItem = document.querySelector('.nav-item'); if (divisionesNavItem) { const divisionesLink = divisionesNavItem.querySelector('.nav-link-group .nav-link'); if (divisionesLink) { // Prevenir comportamiento por defecto del enlace divisionesLink.addEventListener('click', (e) => { if (window.innerWidth <= 768) { e.preventDefault(); e.stopPropagation(); // Toggle del submenú divisionesNavItem.classList.toggle('open'); // Actualizar aria-expanded del botón toggle const toggleBtn = divisionesNavItem.querySelector('.dropdown-toggle'); if (toggleBtn) { toggleBtn.setAttribute('aria-expanded', divisionesNavItem.classList.contains('open')); } } }); } } // Smooth scroll solo para enlaces que NO son el de Divisiones en móvil document.querySelectorAll('a[href^="#"]').forEach(link => { // Verificar si es el enlace de Divisiones en móvil const isDivisionesLink = window.innerWidth <= 768 && link.closest('.nav-item') && link.closest('.nav-item') === divisionesNavItem && link.getAttribute('href') === '#divisions'; if (isDivisionesLink) { return; // No agregar smooth scroll a este enlace } link.addEventListener('click', (e) => { const target = link.getAttribute('href'); if (target && target !== '#') { e.preventDefault(); smoothScroll(target); // Cerrar menú móvil si está abierto if (window.innerWidth <= 768 && navLinksContainer.classList.contains('active')) { navLinksContainer.classList.remove('active'); mobileMenuBtn.classList.remove('active'); } } }); }); // Mobile menu toggle (hamburguesa) mobileMenuBtn.addEventListener('click', () => { navLinksContainer.classList.toggle('active'); mobileMenuBtn.classList.toggle('active'); // Cerrar cualquier submenú abierto al cerrar el menú principal if (!navLinksContainer.classList.contains('active')) { const openSubmenus = document.querySelectorAll('.nav-item.open'); openSubmenus.forEach(submenu => { submenu.classList.remove('open'); const toggleBtn = submenu.querySelector('.dropdown-toggle'); if (toggleBtn) { toggleBtn.setAttribute('aria-expanded', 'false'); } }); } }); // Toggle dropdown desde el botón flecha (consistencia) document.querySelectorAll('.dropdown-toggle').forEach(trigger => { trigger.addEventListener('click', (e) => { if (window.innerWidth <= 768) { e.preventDefault(); e.stopPropagation(); const item = trigger.closest('.nav-item'); if (item) { item.classList.toggle('open'); trigger.setAttribute('aria-expanded', item.classList.contains('open')); } } }); }); // Cerrar menú móvil al hacer clic fuera document.addEventListener('click', (e) => { if (!e.target.closest('.navbar') && navLinksContainer.classList.contains('active')) { navLinksContainer.classList.remove('active'); mobileMenuBtn.classList.remove('active'); } }); // Cerrar submenús al redimensionar a desktop window.addEventListener('resize', () => { if (window.innerWidth > 768) { const openSubmenus = document.querySelectorAll('.nav-item.open'); openSubmenus.forEach(submenu => { submenu.classList.remove('open'); }); } }); } // Scroll Animations function setupScrollAnimations() { const observerOptions = { threshold: 0.1, rootMargin: '0px 0px -100px 0px' }; const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.style.opacity = '1'; entry.target.style.transform = 'translateY(0)'; } }); }, observerOptions); const animatedElements = document.querySelectorAll('[data-aos]'); animatedElements.forEach(el => observer.observe(el)); } // Active Navigation Link on Scroll function setupActiveNavOnScroll() { const sections = document.querySelectorAll('section[id]'); const navLinks = document.querySelectorAll('.nav-link'); window.addEventListener('scroll', () => { let current = ''; sections.forEach(section => { const sectionTop = section.offsetTop; const sectionHeight = section.clientHeight; if (window.pageYOffset >= sectionTop - 100) { current = section.getAttribute('id'); } }); navLinks.forEach(link => { link.classList.remove('active'); if (link.getAttribute('href') === `#${current}`) { link.classList.add('active'); } }); }); } // Initialize everything when DOM is loaded document.addEventListener('DOMContentLoaded', () => { console.log('DOM loaded, initializing...'); // Initialize particle network with colors from data attribute or defaults const canvas = document.getElementById('particleCanvas'); if (canvas) { console.log('Canvas found, initializing particles...'); const particleColor = canvas.getAttribute('data-particle-color'); const lineColor = canvas.getAttribute('data-line-color'); const colorConfig = (particleColor && lineColor) ? { particle: particleColor, line: lineColor } : null; new ParticleNetwork(colorConfig); } else { console.error('Canvas not found!'); } // Initialize text rotator const textContainer = document.querySelector('.animated-text-container'); if (textContainer) { console.log('Text container found, initializing rotator...'); new TextRotator('.animated-text-container'); } else { console.warn('Text container not found'); } // Setup navigation setupNavigation(); // Setup scroll animations setupScrollAnimations(); // Setup active nav on scroll setupActiveNavOnScroll(); setupMobileSubmenuClose(); console.log('Initialization complete!'); }); // Add parallax effect to hero section window.addEventListener('scroll', () => { const hero = document.querySelector('.hero-content'); const scrolled = window.pageYOffset; if (hero && scrolled < window.innerHeight) { hero.style.transform = `translateY(${scrolled * 0.5}px)`; hero.style.opacity = 1 - (scrolled / window.innerHeight); } }); // ============================================================ // Carrusel de Logos - Pausa al hover // ============================================================ function setupLogoCarousel() { const track = document.querySelector('.logo-track'); const logoItems = document.querySelectorAll('.logo-item'); if (!track) return; // Pausar animación al hacer hover en cualquier logo logoItems.forEach(item => { item.addEventListener('mouseenter', () => { track.style.animationPlayState = 'paused'; }); item.addEventListener('mouseleave', () => { track.style.animationPlayState = 'running'; }); }); } // Inicializar if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', setupLogoCarousel); } else { setupLogoCarousel(); } // Cerrar submenú al seleccionar una opción (en móvil) function setupMobileSubmenuClose() { if (window.innerWidth <= 768) { const dropdownLinks = document.querySelectorAll('.dropdown-link'); dropdownLinks.forEach(link => { link.addEventListener('click', () => { // Cerrar el submenú después de hacer clic const navItem = link.closest('.nav-item'); if (navItem) { navItem.classList.remove('open'); const toggleBtn = navItem.querySelector('.dropdown-toggle'); if (toggleBtn) { toggleBtn.setAttribute('aria-expanded', 'false'); } } // Opcional: cerrar también el menú principal si quieres const navLinksContainer = document.getElementById('navLinks'); const mobileMenuBtn = document.getElementById('mobileMenuBtn'); if (navLinksContainer && mobileMenuBtn && navLinksContainer.classList.contains('active')) { navLinksContainer.classList.remove('active'); mobileMenuBtn.classList.remove('active'); } }); }); } } // ============================================================ // Botón Flotante de WhatsApp con Transición Visual // ============================================================ document.addEventListener('DOMContentLoaded', function () { const whatsappFloat = document.getElementById('whatsappFloat'); const heroSection = document.getElementById('home'); const navbar = document.querySelector('.navbar'); if (!whatsappFloat || !heroSection) return; let currentState = 'large'; // 'large' o 'small' let isTransitioning = false; // Función para verificar si el elemento está en viewport function isElementInViewport(el, offset = 0) { const rect = el.getBoundingClientRect(); const windowHeight = window.innerHeight || document.documentElement.clientHeight; return rect.top <= windowHeight - offset && rect.bottom >= offset; } // Función para verificar si estamos en el hero function isInHero() { return isElementInViewport(heroSection, 100); } // Función para verificar si el usuario ha pasado el hero function hasScrolledPastHero() { const heroBottom = heroSection.getBoundingClientRect().bottom; const navHeight = navbar ? navbar.offsetHeight : 80; return heroBottom <= navHeight + 50; } // Función principal para actualizar el estado con efecto visual function updateWhatsAppButton() { if (isTransitioning) return; // Evitar multiples disparos a la vez const inHero = isInHero(); const scrolledPast = hasScrolledPastHero(); const newState = (inHero && !scrolledPast) ? 'large' : 'small'; // Solo cambiar si el estado es diferente if (newState !== currentState) { isTransitioning = true; // 1. Iniciar teletransportación fuera whatsappFloat.classList.add('teleporting-out'); // 2. Esperar a que desaparezca setTimeout(() => { // Limpiar clases de estado whatsappFloat.classList.remove('large', 'small', 'teleporting-out', 'initial'); // Cambiar la posición instantaneamente siendo invisibles whatsappFloat.classList.add(newState); // 3. Iniciar teletransportación entrada whatsappFloat.classList.add('teleporting-in'); currentState = newState; // 4. Limpiar cuando termina setTimeout(() => { whatsappFloat.classList.remove('teleporting-in'); isTransitioning = false; }, 400); // 400ms para aparecer }, 300); // 300ms para desaparecer // Log para debug (opcional) console.log(`Botón teletransportado a: ${newState}`); } } // Throttle para mejor performance let ticking = false; let lastScrollY = window.scrollY; let scrollDirection = 'down'; window.addEventListener('scroll', function () { // Detectar dirección del scroll para efectos adicionales const currentScrollY = window.scrollY; scrollDirection = currentScrollY > lastScrollY ? 'down' : 'up'; lastScrollY = currentScrollY; if (!ticking) { requestAnimationFrame(function () { updateWhatsAppButton(); ticking = false; }); ticking = true; } }); // Actualizar en resize window.addEventListener('resize', function () { updateWhatsAppButton(); }); // Actualizar en la carga inicial updateWhatsAppButton(); // Efecto adicional: mostrar notificación visual cuando cambia de posición // (opcional: agregar un pequeño badge que indique el cambio) let hasShownNotification = false; function showPositionNotification() { if (hasShownNotification) return; // Crear un pequeño indicador visual (opcional) /*const notification = document.createElement('div'); notification.style.cssText = ` position: fixed; bottom: 110px; right: 110px; background: rgba(110, 124, 181, 0.8); color: white; padding: 5px 12px; border-radius: 20px; font-size: 12px; font-family: 'Space Grotesk', sans-serif; z-index: 999; opacity: 0; transition: opacity 0.3s ease; pointer-events: none; white-space: nowrap; `;*/ document.body.appendChild(notification); setTimeout(() => { notification.style.opacity = '1'; setTimeout(() => { notification.style.opacity = '0'; setTimeout(() => { notification.remove(); }, 300); }, 2000); }, 100); hasShownNotification = true; } // Mostrar notificación la primera vez que sale del hero let hasTriggeredNotification = false; function checkAndShowNotification() { if (!hasTriggeredNotification && currentState === 'small') { showPositionNotification(); hasTriggeredNotification = true; } } // Monitorear cambios de estado para la notificación const observer = new MutationObserver(function (mutations) { mutations.forEach(function (mutation) { if (mutation.attributeName === 'class') { checkAndShowNotification(); } }); }); observer.observe(whatsappFloat, { attributes: true }); });