/* ===================================================================== ONAPB — Kinetic FX Capa de interacciones del frontend. Vanilla JS, sin dependencias. Cargar al final de con defer. ===================================================================== */ (function () { 'use strict'; var prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; var hasFinePointer = window.matchMedia('(hover: hover) and (pointer: fine)').matches; var isTouch = !hasFinePointer || ('ontouchstart' in window) || (navigator.maxTouchPoints > 0); function ready(fn) { if (document.readyState !== 'loading') fn(); else document.addEventListener('DOMContentLoaded', fn); } /* ───────────────────────────────────────────────────────────────── 1. Reveal on scroll (.kfx-reveal, [data-stagger], .kfx-section-tag) ───────────────────────────────────────────────────────────────── */ function initReveal() { if (!('IntersectionObserver' in window)) { document.querySelectorAll('.kfx-reveal, [data-stagger], .kfx-section-tag, .hero-title, .kinetic-reveal-text, .kinetic-highlight') .forEach(function (el) { el.classList.add('is-in'); }); return; } var io = new IntersectionObserver(function (entries) { entries.forEach(function (e) { if (e.isIntersecting) { e.target.classList.add('is-in'); io.unobserve(e.target); } }); }, { threshold: 0.12, rootMargin: '0px 0px -60px 0px' }); document.querySelectorAll('.kfx-reveal, [data-stagger], .kfx-section-tag, .hero-title, .kinetic-reveal-text, .kinetic-highlight') .forEach(function (el) { io.observe(el); }); } /* ───────────────────────────────────────────────────────────────── 2. Word splitter para reveal de títulos (hero-title, kinetic-reveal-text) ───────────────────────────────────────────────────────────────── */ function splitWords() { document.querySelectorAll('.hero-title:not([data-split]), .kinetic-reveal-text:not([data-split])').forEach(function (el) { if (el.children.length > 0 && el.querySelector('.word')) return; var text = el.textContent.trim(); if (!text) return; var words = text.split(/\s+/); el.innerHTML = words.map(function (w) { return '' + w + ''; }).join(' '); el.setAttribute('data-split', 'true'); }); } /* ───────────────────────────────────────────────────────────────── 3. Reading progress bar (top) ───────────────────────────────────────────────────────────────── */ function initReadingBar() { if (prefersReduced) return; var bar = document.getElementById('kinetic-reading-bar'); if (!bar) { bar = document.createElement('div'); bar.id = 'kinetic-reading-bar'; document.body.appendChild(bar); } var ticking = false; function update() { var scrollTop = window.scrollY || document.documentElement.scrollTop; var docHeight = document.documentElement.scrollHeight - window.innerHeight; var pct = docHeight > 0 ? Math.min(100, (scrollTop / docHeight) * 100) : 0; bar.style.width = pct + '%'; ticking = false; } window.addEventListener('scroll', function () { if (!ticking) { window.requestAnimationFrame(update); ticking = true; } }, { passive: true }); update(); } /* ───────────────────────────────────────────────────────────────── 4. Navbar scroll state ───────────────────────────────────────────────────────────────── */ function initNavbarScroll() { var nav = document.querySelector('.custom-navbar'); if (!nav) return; function update() { if (window.scrollY > 30) nav.classList.add('is-scrolled'); else nav.classList.remove('is-scrolled'); } window.addEventListener('scroll', update, { passive: true }); update(); } /* ───────────────────────────────────────────────────────────────── 5. Spotlight para .kinetic-card y .kfx-img-spotlight ───────────────────────────────────────────────────────────────── */ function initSpotlight() { if (isTouch) return; var els = document.querySelectorAll('.kinetic-card, .kfx-img-spotlight'); els.forEach(function (el) { el.addEventListener('mousemove', function (e) { var rect = el.getBoundingClientRect(); var x = ((e.clientX - rect.left) / rect.width) * 100; var y = ((e.clientY - rect.top) / rect.height) * 100; el.style.setProperty('--mx', x + '%'); el.style.setProperty('--my', y + '%'); }); }); } /* ───────────────────────────────────────────────────────────────── 6. Tilt 3D (data-tilt) ───────────────────────────────────────────────────────────────── */ function initTilt() { if (isTouch || prefersReduced) return; document.querySelectorAll('[data-tilt]').forEach(function (el) { var max = parseFloat(el.getAttribute('data-tilt-max') || '8'); el.addEventListener('mousemove', function (e) { var rect = el.getBoundingClientRect(); var x = (e.clientX - rect.left) / rect.width - 0.5; var y = (e.clientY - rect.top) / rect.height - 0.5; el.classList.add('is-tilting'); el.style.setProperty('--ry', (x * max) + 'deg'); el.style.setProperty('--rx', (-y * max) + 'deg'); }); el.addEventListener('mouseleave', function () { el.classList.remove('is-tilting'); el.style.setProperty('--rx', '0deg'); el.style.setProperty('--ry', '0deg'); }); }); } /* ───────────────────────────────────────────────────────────────── 7. Magnetic buttons (data-magnetic) ───────────────────────────────────────────────────────────────── */ function initMagnetic() { if (isTouch || prefersReduced) return; document.querySelectorAll('[data-magnetic]').forEach(function (el) { var strength = parseFloat(el.getAttribute('data-magnetic-strength') || '0.25'); el.addEventListener('mousemove', function (e) { var rect = el.getBoundingClientRect(); var x = e.clientX - rect.left - rect.width / 2; var y = e.clientY - rect.top - rect.height / 2; el.style.transform = 'translate(' + (x * strength) + 'px, ' + (y * strength) + 'px)'; }); el.addEventListener('mouseleave', function () { el.style.transform = ''; }); }); } /* ───────────────────────────────────────────────────────────────── 8. Ripple en clicks (cualquier .btn, .btn-kinetic-primary) ───────────────────────────────────────────────────────────────── */ function initRipple() { document.addEventListener('click', function (e) { var btn = e.target.closest('.btn, .btn-kinetic-primary, .btn-admin-primary, .sidebar-link'); if (!btn || btn.classList.contains('no-ripple')) return; var rect = btn.getBoundingClientRect(); var size = Math.max(rect.width, rect.height); var wave = document.createElement('span'); wave.className = 'kinetic-ripple-wave'; wave.style.width = wave.style.height = size + 'px'; wave.style.left = (e.clientX - rect.left) + 'px'; wave.style.top = (e.clientY - rect.top) + 'px'; // Posición relativa para contener el ripple var pos = window.getComputedStyle(btn).position; if (pos === 'static') btn.style.position = 'relative'; btn.style.overflow = 'hidden'; btn.appendChild(wave); setTimeout(function () { wave.remove(); }, 650); }); } /* ───────────────────────────────────────────────────────────────── 9. Counter animado (.kfx-counter con data-target) ───────────────────────────────────────────────────────────────── */ function initCounters() { if (!('IntersectionObserver' in window)) return; var counters = document.querySelectorAll('.kfx-counter[data-target]'); if (!counters.length) return; var io = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (!entry.isIntersecting) return; var el = entry.target; var target = parseFloat(el.getAttribute('data-target')) || 0; var duration = parseInt(el.getAttribute('data-duration') || '1400', 10); var decimals = parseInt(el.getAttribute('data-decimals') || '0', 10); var prefix = el.getAttribute('data-prefix') || ''; var suffix = el.getAttribute('data-suffix') || ''; var start = 0; var startTime = null; if (prefersReduced) { el.textContent = prefix + target.toFixed(decimals) + suffix; io.unobserve(el); return; } function tick(ts) { if (!startTime) startTime = ts; var progress = Math.min(1, (ts - startTime) / duration); var eased = 1 - Math.pow(1 - progress, 3); // easeOutCubic var current = start + (target - start) * eased; el.textContent = prefix + current.toFixed(decimals) + suffix; if (progress < 1) requestAnimationFrame(tick); } requestAnimationFrame(tick); io.unobserve(el); }); }, { threshold: 0.4 }); counters.forEach(function (c) { io.observe(c); }); } /* ───────────────────────────────────────────────────────────────── 10. Parallax suave (data-parallax="speed") ───────────────────────────────────────────────────────────────── */ function initParallax() { if (prefersReduced) return; var els = Array.prototype.slice.call(document.querySelectorAll('[data-parallax]')); if (!els.length) return; var ticking = false; function update() { var vh = window.innerHeight; els.forEach(function (el) { var rect = el.getBoundingClientRect(); if (rect.bottom < 0 || rect.top > vh) return; var speed = parseFloat(el.getAttribute('data-parallax')) || 0.2; var offset = (rect.top - vh / 2) * speed * -1; el.style.transform = 'translate3d(0, ' + offset.toFixed(1) + 'px, 0)'; }); ticking = false; } window.addEventListener('scroll', function () { if (!ticking) { requestAnimationFrame(update); ticking = true; } }, { passive: true }); update(); } /* ───────────────────────────────────────────────────────────────── 11. Marquee duplicador (.kfx-marquee con un solo track) ───────────────────────────────────────────────────────────────── */ function initMarquee() { document.querySelectorAll('.kfx-marquee').forEach(function (el) { var track = el.querySelector('.kfx-marquee__track'); if (!track || track.dataset.cloned === '1') return; track.dataset.cloned = '1'; track.innerHTML += track.innerHTML; // duplicar para loop seamless }); } /* ───────────────────────────────────────────────────────────────── 12. Cursor personalizado (sólo desktop hover-fino) ───────────────────────────────────────────────────────────────── */ function initCustomCursor() { if (!hasFinePointer || isTouch || prefersReduced) return; if (document.querySelector('.kinetic-cursor-dot')) return; var dot = document.createElement('div'); var ring = document.createElement('div'); dot.className = 'kinetic-cursor-dot'; ring.className = 'kinetic-cursor-ring'; document.body.appendChild(dot); document.body.appendChild(ring); document.body.classList.add('kinetic-cursor-on'); var mx = window.innerWidth / 2, my = window.innerHeight / 2; var rx = mx, ry = my; document.addEventListener('mousemove', function (e) { mx = e.clientX; my = e.clientY; dot.style.transform = 'translate(' + mx + 'px, ' + my + 'px) translate(-50%, -50%)'; }, { passive: true }); function loop() { rx += (mx - rx) * 0.18; ry += (my - ry) * 0.18; ring.style.transform = 'translate(' + rx + 'px, ' + ry + 'px) translate(-50%, -50%)'; requestAnimationFrame(loop); } loop(); // Hover en interactivos var hoverSel = 'a, button, .btn, .kinetic-card, [role="button"], [data-tilt], [data-magnetic], .nav-link, input[type="submit"]'; document.addEventListener('mouseover', function (e) { if (e.target.closest(hoverSel)) { ring.classList.add('is-hover'); dot.classList.add('is-hover'); } }); document.addEventListener('mouseout', function (e) { if (e.target.closest(hoverSel)) { ring.classList.remove('is-hover'); dot.classList.remove('is-hover'); } }); document.addEventListener('mouseleave', function () { ring.style.opacity = '0'; dot.style.opacity = '0'; }); document.addEventListener('mouseenter', function () { ring.style.opacity = '1'; dot.style.opacity = '1'; }); } /* ───────────────────────────────────────────────────────────────── 13. Auto-aplicar reveal a tarjetas / encabezados (best-effort) ───────────────────────────────────────────────────────────────── */ function autoTagReveal() { // Tarjetas comunes en home/eventos document.querySelectorAll('.kinetic-card:not(.kfx-reveal)').forEach(function (el) { el.classList.add('kfx-reveal'); if (!el.hasAttribute('data-fx')) el.setAttribute('data-fx', 'up'); }); // Display headings (h1.display-* / h2.display-*) document.querySelectorAll('h1.display-1, h1.display-2, h1.display-3, h1.display-4, h2.display-3, h2.display-4, h2.display-5') .forEach(function (el) { if (!el.classList.contains('kfx-reveal') && !el.classList.contains('hero-title')) { el.classList.add('kfx-reveal'); el.setAttribute('data-fx', 'up'); } }); } /* ───────────────────────────────────────────────────────────────── 14. Smooth anchor scroll ───────────────────────────────────────────────────────────────── */ function initAnchorSmooth() { document.addEventListener('click', function (e) { var a = e.target.closest('a[href^="#"]'); if (!a) return; var href = a.getAttribute('href'); if (!href || href === '#' || href.length < 2) return; var target = document.querySelector(href); if (!target) return; e.preventDefault(); target.scrollIntoView({ behavior: prefersReduced ? 'auto' : 'smooth', block: 'start' }); }); } /* ───────────────────────────────────────────────────────────────── 15. SweetAlert2 patcher (a prueba de cache) Detecta cuando aparece un .swal2-popup y fuerza inline styles en los botones para garantizar fondo rojo en confirm y gris en cancel, sin importar el orden/cache del CSS o el confirmButtonColor del JS. ───────────────────────────────────────────────────────────────── */ function initSwalPatcher() { if (!('MutationObserver' in window)) return; var BRAND_RED = '#c20000'; var BRAND_RED_DEEP = '#8a0000'; var BRAND_GRAY = '#6c757d'; // Inyectar reglas críticas como