primer desarrollo

This commit is contained in:
2026-08-20 23:57:02 -03:00
commit d4368d3378
56 changed files with 7868 additions and 0 deletions
+622
View File
@@ -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 });
});