Files
Laucha1312 fdd0fef3f0 2354
2026-06-04 15:19:42 -03:00

481 lines
27 KiB
JavaScript

/* =====================================================================
ONAPB — Kinetic FX
Capa de interacciones del frontend.
Vanilla JS, sin dependencias. Cargar al final de <body> 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 '<span class="word"><span>' + w + '</span></span>';
}).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 <style> en runtime para evitar
// problemas de caché del archivo .css (LiteSpeed/Cloudflare).
if (!document.getElementById('kfx-swal-runtime-style')) {
var s = document.createElement('style');
s.id = 'kfx-swal-runtime-style';
s.textContent = [
/* Selección visible dentro del modal SweetAlert (rojo + texto blanco). */
'.swal2-popup ::selection,.swal2-popup *::selection{background-color:' + BRAND_RED + ' !important;color:#fff !important;}',
'.swal2-popup ::-moz-selection,.swal2-popup *::-moz-selection{background-color:' + BRAND_RED + ' !important;color:#fff !important;}',
/* Selección global (failsafe por si el CSS principal cachea viejo). */
'::selection{background-color:' + BRAND_RED + ' !important;color:#fff !important;}',
'::-moz-selection{background-color:' + BRAND_RED + ' !important;color:#fff !important;}',
/* Botones de SweetAlert (failsafe). */
'body .swal2-popup .swal2-actions .swal2-styled.swal2-confirm{' +
'background-color:' + BRAND_RED + ' !important;' +
'background-image:linear-gradient(135deg,' + BRAND_RED + ',' + BRAND_RED_DEEP + ') !important;' +
'color:#fff !important;border:none !important;' +
'}',
'body .swal2-popup .swal2-actions .swal2-styled.swal2-cancel{' +
'background-color:' + BRAND_GRAY + ' !important;' +
'background-image:none !important;' +
'color:#fff !important;border:none !important;' +
'}'
].join('\n');
document.head.appendChild(s);
}
function styleConfirm(btn) {
if (!btn) return;
btn.style.setProperty('background-color', BRAND_RED, 'important');
btn.style.setProperty('background-image',
'linear-gradient(135deg, ' + BRAND_RED + ', ' + BRAND_RED_DEEP + ')', 'important');
btn.style.setProperty('color', '#ffffff', 'important');
btn.style.setProperty('border', 'none', 'important');
btn.style.setProperty('text-shadow', 'none', 'important');
btn.style.setProperty('font-weight', '700', 'important');
btn.style.setProperty('text-transform', 'uppercase', 'important');
btn.style.setProperty('letter-spacing', '0.08em', 'important');
}
function styleCancel(btn) {
if (!btn) return;
btn.style.setProperty('background-color', BRAND_GRAY, 'important');
btn.style.setProperty('background-image', 'none', 'important');
btn.style.setProperty('color', '#ffffff', 'important');
btn.style.setProperty('border', 'none', 'important');
btn.style.setProperty('font-weight', '700', 'important');
btn.style.setProperty('text-transform', 'uppercase', 'important');
btn.style.setProperty('letter-spacing', '0.08em', 'important');
}
function styleDeny(btn) {
if (!btn) return;
btn.style.setProperty('background-color', '#b91c1c', 'important');
btn.style.setProperty('color', '#ffffff', 'important');
}
function patch(popup) {
if (!popup || popup.dataset.kfxPatched === '1') return;
popup.dataset.kfxPatched = '1';
styleConfirm(popup.querySelector('.swal2-confirm'));
styleCancel(popup.querySelector('.swal2-cancel'));
styleDeny(popup.querySelector('.swal2-deny'));
}
// Patch popups que ya estén en el DOM al cargar
document.querySelectorAll('.swal2-popup').forEach(patch);
// Observa nuevos popups (SWAL los agrega/quita dinámicamente)
var obs = new MutationObserver(function (mutations) {
mutations.forEach(function (m) {
m.addedNodes && m.addedNodes.forEach(function (n) {
if (!(n instanceof HTMLElement)) return;
if (n.classList && n.classList.contains('swal2-popup')) {
patch(n);
} else {
// Por si el contenedor agrega un wrapper antes del popup
var inner = n.querySelector && n.querySelector('.swal2-popup');
if (inner) patch(inner);
}
});
});
});
obs.observe(document.body, { childList: true, subtree: true });
}
/* ─────────────────────────────────────────────────────────────────
Init
───────────────────────────────────────────────────────────────── */
ready(function () {
try { splitWords(); } catch (e) { console.warn('kinetic-fx splitWords:', e); }
try { autoTagReveal(); } catch (e) { console.warn('kinetic-fx autoTagReveal:', e); }
try { initReveal(); } catch (e) { console.warn('kinetic-fx reveal:', e); }
try { initReadingBar(); } catch (e) { console.warn('kinetic-fx readingBar:', e); }
try { initNavbarScroll(); } catch (e) { console.warn('kinetic-fx navbarScroll:', e); }
try { initSpotlight(); } catch (e) { console.warn('kinetic-fx spotlight:', e); }
try { initTilt(); } catch (e) { console.warn('kinetic-fx tilt:', e); }
try { initMagnetic(); } catch (e) { console.warn('kinetic-fx magnetic:', e); }
try { initRipple(); } catch (e) { console.warn('kinetic-fx ripple:', e); }
try { initCounters(); } catch (e) { console.warn('kinetic-fx counters:', e); }
try { initParallax(); } catch (e) { console.warn('kinetic-fx parallax:', e); }
try { initMarquee(); } catch (e) { console.warn('kinetic-fx marquee:', e); }
try { initCustomCursor(); } catch (e) { console.warn('kinetic-fx cursor:', e); }
try { initAnchorSmooth(); } catch (e) { console.warn('kinetic-fx anchor:', e); }
try { initSwalPatcher(); } catch (e) { console.warn('kinetic-fx swalPatcher:', e); }
});
// API pública mínima (por si una vista necesita re-inicializar tras AJAX)
window.KineticFX = {
rescan: function () {
splitWords();
autoTagReveal();
initReveal();
initSpotlight();
initTilt();
initMagnetic();
initCounters();
initMarquee();
}
};
})();