1/** 2 * INRAI · Carruseles del sitio 3 * 4 * 1) Ad carousel â rotación crossfade de anunciantes en cada slot. 5 * 2) Hero carousel â 6 noticias destacadas con autoplay, dots con barra 6 * de progreso, navegación por teclado (â/â) y swipe táctil. 7 * 8 * Idempotente: cada root se marca con data-*-init="1" para no doble-bind 9 * cuando el soft-nav router re-inicializa tras swap de #main-content. 10 */ 11 12/* ââ 1) Ad carousel ââââââââââââââââââââââââââââââââââââââââââââ */ 13/* Transición ÃNICA "vidrio roto" para toda publicidad: al avanzar de un 14 * slide al siguiente, capturamos la imagen del banner saliente, la dividimos 15 * en una grilla de fragmentos con clip-path irregular y los hacemos caer 16 * con rotación + gravedad. Mientras caen, debajo ya está visible el banner 17 * entrante. Los fragmentos se generan con un jitter aleatorio por 18 * transición para que cada rotura sea diferente. */ 19(() => { 20 const FX_DURATION = 1200; 21 const COLS = 5; 22 const ROWS = 3; 23 24 const rand = (a, b) => a + Math.random() * (b - a); 25 const jitter = (v, r) => v + (Math.random() - 0.5) * r; 26 27 /** 28 * PolÃgono octagonal con vértices ligeramente perturbados, en 29 * coordenadas porcentuales del overlay (0-100). Genera una pieza 30 * dentro de una celda (c, r) de la grilla COLSÃROWS. 31 */ 32 const shardPolygon = (c, r) => { 33 const x0 = (c / COLS) * 100; 34 const x1 = ((c + 1) / COLS) * 100; 35 const y0 = (r / ROWS) * 100; 36 const y1 = ((r + 1) / ROWS) * 100; 37 const mx = (x0 + x1) / 2; 38 const my = (y0 + y1) / 2; 39 const j = 3; // jitter porcentual sobre vértices 40 return `polygon( 41 ${jitter(x0, j)}% ${jitter(y0, j)}%, 42 ${jitter(mx, j)}% ${jitter(y0, j * 1.4)}%, 43 ${jitter(x1, j)}% ${jitter(y0, j)}%, 44 ${jitter(x1, j * 1.4)}% ${jitter(my, j)}%, 45 ${jitter(x1, j)}% ${jitter(y1, j)}%, 46 ${jitter(mx, j)}% ${jitter(y1, j * 1.4)}%, 47 ${jitter(x0, j)}% ${jitter(y1, j)}%, 48 ${jitter(x0, j * 1.4)}% ${jitter(my, j)}% 49 )`; 50 }; 51 52 const shatter = (root, outgoing) => { 53 const img = outgoing.querySelector('img'); 54 if (!img) return; 55 const url = img.currentSrc || img.src; 56 if (!url) return; 57 58 const overlay = document.createElement('div'); 59 overlay.className = 'glass-overlay'; 60 overlay.setAttribute('aria-hidden', 'true'); 61 62 // Punto de impacto aleatorio cerca del centro 63 const impactX = rand(38, 62); 64 const impactY = rand(38, 62); 65 66 const crack = document.createElement('div'); 67 crack.className = 'glass-crack'; 68 crack.style.setProperty('--impact-x', `${impactX}%`); 69 crack.style.setProperty('--impact-y', `${impactY}%`); 70 overlay.appendChild(crack); 71 72 for (let r = 0; r < ROWS; r++) { 73 for (let c = 0; c < COLS; c++) { 74 const shard = document.createElement('span'); 75 shard.className = 'glass-shard'; 76 77 // Centroide aproximado de la celda 78 const cx = (c + 0.5) / COLS * 100; 79 const cy = (r + 0.5) / ROWS * 100; 80 81 // Vector de explosión desde el punto de impacto (kick inicial) 82 const dx = cx - impactX; 83 const dy = cy - impactY; 84 const len = Math.hypot(dx, dy) || 1; 85 const kick = rand(10, 22); 86 const kickX = (dx / len) * kick; 87 const kickY = (dy / len) * kick; 88 89 // CaÃda: empuje horizontal residual + gravedad fuerte en Y 90 const drift = (dx / len) * rand(20, 80); 91 const fall = rand(220, 380) + Math.abs(dy) * rand(1.4, 2.2); 92 const rot = rand(-260, 260); 93 const delay = Math.random() * 180; 94 95 shard.style.setProperty('--img', `url("${url.replace(/"/g, '\\"')}")`); 96 shard.style.setProperty('--shape', shardPolygon(c, r)); 97 shard.style.setProperty('--origin-x', `${cx}%`); 98 shard.style.setProperty('--origin-y', `${cy}%`); 99 shard.style.setProperty('--kick-x', `${kickX}px`); 100 shard.style.setProperty('--kick-y', `${kickY}px`); 101 shard.style.setProperty('--tx', `${drift}px`); 102 shard.style.setProperty('--ty', `${fall}px`); 103 shard.style.setProperty('--rot', `${rot}deg`); 104 shard.style.setProperty('--delay', `${delay}ms`); 105 106 overlay.appendChild(shard); 107 } 108 } 109 110 root.appendChild(overlay); 111 window.setTimeout(() => overlay.remove(), FX_DURATION); 112 }; 113 114 const initAdCarousel = (root) => { 115 if (root.dataset.adCarouselInit === '1') return; 116 const slides = [...root.querySelectorAll('.ad-carousel__slide')]; 117 const counter = root.querySelector('[data-ad-current]'); 118 if (slides.length < 2) return; 119 root.dataset.adCarouselInit = '1'; 120 let current = 0; 121 let timer; 122 let busy = false;
123 124 const advance = () => { 125 if (busy) return; 126 busy = true; 127 const outgoing = slides[current]; 128 const nextIdx = (current + 1) % slides.length; 129 const incoming = slides[nextIdx]; 130 131 // El entrante se hace visible primero (queda debajo del overlay). 132 incoming.classList.add('is-active'); 133 shatter(root, outgoing); 134 135 // El saliente sale del flujo cuando los pedazos ya cubren el 136 // banner; asà no hay flash de "cambio brusco" antes del efecto. 137 window.setTimeout(() => { 138 outgoing.classList.remove('is-active'); 139 }, 60); 140 141 current = nextIdx; 142 if (counter) counter.textContent = current + 1; 143 window.setTimeout(() => { busy = false; }, FX_DURATION); 144 }; 145 146 const start = () => { timer = window.setInterval(advance, 6000); }; 147 const stop = () => { window.clearInterval(timer); }; 148 149 root.addEventListener('mouseenter', stop); 150 root.addEventListener('mouseleave', start); 151 start(); 152 }; 153 154 const initAll = () => { 155 document.querySelectorAll('[data-ad-carousel]').forEach(initAdCarousel); 156 }; 157 158 if (document.readyState === 'loading') { 159 document.addEventListener('DOMContentLoaded', initAll); 160 } else { 161 initAll(); 162 } 163 document.addEventListener('nav-soft:loaded', initAll); 164})(); 165 166/* ââ 2) Hero carousel ââââââââââââââââââââââââââââââââââââââââââ */ 167(() => { 168 const initHero = (root) => { 169 if (!root || root.dataset.heroInit === '1') return; 170 const slides = [...root.querySelectorAll('.hero__slide')]; 171 const dots = [...root.querySelectorAll('[data-hero-dot]')]; 172 const prev = root.querySelector('[data-hero-prev]'); 173 const next = root.querySelector('[data-hero-next]'); 174 if (slides.length < 2) return; 175 root.dataset.heroInit = '1'; 176 177 const duration = parseInt(root.dataset.autoplay, 10) || 6000; 178 root.style.setProperty('--hero-duration', `${duration}ms`); 179 180 let index = slides.findIndex(s => s.classList.contains('is-active')); 181 if (index < 0) index = 0; 182 let timer = null; 183 let paused = false; 184 185 const go = (target) => { 186 const n = (target + slides.length) % slides.length; 187 if (n === index) return; 188 189 const out = slides[index]; 190 const into = slides[n]; 191 out.classList.remove('is-active'); 192 out.setAttribute('aria-hidden', 'true'); 193 out.setAttribute('tabindex', '-1'); 194 into.classList.add('is-active'); 195 into.removeAttribute('aria-hidden'); 196 into.removeAttribute('tabindex'); 197 198 dots[index].classList.remove('is-active'); 199 dots[index].setAttribute('aria-selected', 'false'); 200 dots[n].classList.add('is-active'); 201 dots[n].setAttribute('aria-selected', 'true'); 202 203 index = n; 204 }; 205 206 const stop = () => { clearTimeout(timer); timer = null; }; 207 const play = () => { 208 stop(); 209 if (paused) return; 210 timer = setTimeout(() => { go(index + 1); play(); }, duration); 211 }; 212 213 /* Navegación manual */ 214 prev && prev.addEventListener('click', () => { go(index - 1); play(); }); 215 next && next.addEventListener('click', () => { go(index + 1); play(); }); 216 dots.forEach((dot, i) => { 217 dot.addEventListener('click', () => { go(i); play(); }); 218 }); 219 220 /* Pausa al pasar el cursor (también pausa la barra de progreso vÃa CSS) */ 221 root.addEventListener('mouseenter', () => { 222 paused = true; root.dataset.paused = 'true'; stop(); 223 }); 224 root.addEventListener('mouseleave', () => { 225 paused = false; delete root.dataset.paused; play(); 226 }); 227 228 /* Teclado: â â cuando el carrusel tiene foco */ 229 root.addEventListener('keydown', (e) => { 230 if (e.key === 'ArrowLeft') { go(index - 1); play(); } 231 if (e.key === 'ArrowRight') { go(index + 1); play(); } 232 }); 233 234 /* Swipe táctil */ 235 let touchX = null; 236 root.addEventListener('touchstart', (e) => { 237 touchX = e.touches[0].clientX; 238 paused = true; stop(); 239 }, { passive: true }); 240 root.addEventListener('touchend', (e) => { 241 if (touchX === null) return; 242 const dx = e.changedTouches[0].clientX - touchX; 243 if (Math.abs(dx) > 40) go(dx < 0 ? index + 1 : index - 1); 244 touchX = null; 245 paused = false; play(); 246 }); 247 248 /* Pausa cuando la pestaña no está visible (ahorra CPU). 249 * Listener global montado una vez por root vivo. */ 250 const onVis = () => { 251 if (document.hidden) { paused = true; stop(); } 252 else { paused = false; play(); } 253 }; 254 document.addEventListener('visibilitychange', onVis); 255 256 play(); 257 }; 258 259 const initAll = () => { 260 document.querySelectorAll('[data-hero]').forEach(initHero); 261 }; 262 263 if (document.readyState === 'loading') { 264 document.addEventListener('DOMContentLoaded', initAll); 265 } else { 266 initAll(); 267 } 268 document.addEventListener('nav-soft:loaded', initAll); 269})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.