622 lines
21 KiB
JavaScript
622 lines
21 KiB
JavaScript
// 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 });
|
|
}); |