primer desarrollo
This commit is contained in:
+622
@@ -0,0 +1,622 @@
|
||||
// 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 });
|
||||
});
|
||||
Reference in New Issue
Block a user