1// ========================================================= 2// Scroll Reveal Animation Handler (versión extendida: fade + wipe + zoom) 3// ========================================================= 4class ScrollReveal { 5 /** 6 * @param {Object} options 7 * @param {number} [options.threshold=0.15] Porción visible para activar 8 * @param {boolean} [options.reanimate=true] Re-animar al volver a entrar 9 * @param {string} [options.rootMargin="0px 0px -50px 0px"] 10 * @param {boolean} [options.forceMotion=false]Ignora prefers-reduced-motion (debug) 11 */ 12 constructor(options = {}) { 13 this.elements = []; 14 this.windowHeight = window.innerHeight; 15 this.threshold = options.threshold ?? 0.15; 16 this.reanimate = options.reanimate ?? true; 17 this.rootMargin = options.rootMargin ?? "0px 0px -50px 0px"; 18 this.forceMotion = options.forceMotion ?? false; 19 20 const reducePref = window.matchMedia( 21 "(prefers-reduced-motion: reduce)" 22 ).matches; 23 this.reduceMotion = this.forceMotion ? false : reducePref; 24 25 this.init(); 26 } 27 28 init() { 29 // Todas las clases soportadas (reveal + fade + wipe + zoom) 30 this.elements = document.querySelectorAll( 31 [ 32 ".scroll-reveal", 33 ".scroll-reveal-left", 34 ".scroll-reveal-right", 35 ".scroll-reveal-scale", 36 ".scroll-reveal-title", 37 ".scroll-reveal-card", 38 ".scroll-reveal-image", 39 ".scroll-reveal-fade", 40 ".scroll-fade", 41 ".scroll-fade-children", 42 ".scroll-wipe", 43 ".scroll-wipe-left", 44 ".scroll-wipe-right", 45 ".scroll-wipe-top", 46 ".scroll-zoom", 47 ".scroll-zoom-children", 48 ].join(", ") 49 ); 50 51 // Fallback: si no hay IO, deja visibles sin animación 52 if (!("IntersectionObserver" in window)) { 53 this.elements.forEach((el) => el.classList.add("in-view")); 54 return; 55 } 56 57 this.setupIntersectionObserver(); 58 this.setupScrollListener(); 59 this.checkElements(); 60 } 61 62 setupIntersectionObserver() { 63 const options = { threshold: this.threshold, rootMargin: this.rootMargin }; 64 65 this.observer = new IntersectionObserver((entries) => { 66 entries.forEach((entry) => { 67 const el = entry.target; 68 69 if (this.reduceMotion) { 70 this.revealElement(el); 71 return; 72 } 73 74 if (entry.isIntersecting) { 75 this.revealElement(el); 76 } else { 77 this.handleExit(el); 78 } 79 }); 80 }, options); 81 82 this.elements.forEach((el) => this.observer.observe(el)); 83 } 84 85 setupScrollListener() { 86 let ticking = false; 87 const onScroll = () => { 88 if (!ticking) { 89 requestAnimationFrame(() => { 90 this.checkElements(); 91 ticking = false; 92 }); 93 ticking = true; 94 } 95 }; 96 97 window.addEventListener("scroll", onScroll, { passive: true }); 98 window.addEventListener("resize", () => { 99 this.windowHeight = window.innerHeight; 100 this.checkElements(); 101 }); 102 } 103 104 checkElements() { 105 this.elements.forEach((el) => { 106 if (!this.reanimate && el.classList.contains("revealed")) return; 107 108 if (this.isElementInViewport(el)) { 109 this.revealElement(el); 110 } else if (this.reanimate) { 111 this.handleExit(el); 112 } 113 }); 114 } 115 116 isElementInViewport(el) { 117 const rect = el.getBoundingClientRect(); 118 const h = rect.height || 0; 119 const tpx = h * this.threshold; 120 return rect.top <= this.windowHeight - tpx && rect.bottom >= tpx; 121 } 122 123 revealElement(el) { 124 el.classList.add("in-view"); 125 el.classList.remove("out-view"); 126 el.classList.add("revealed"); 127 128 el.dispatchEvent( 129 new CustomEvent("elementRevealed", { detail: { element: el } }) 130 ); 131 } 132 133 handleExit(el) { 134 el.classList.remove("in-view"); 135 136 // Wipe usa .out-view para animar salida 137 if (this.isWipe(el)) el.classList.add("out-view"); 138 139 if (this.reanimate) el.classList.remove("revealed"); 140 } 141 142 resetElement(el) { 143 el.classList.remove("revealed", "in-view", "out-view"); 144 } 145 146 addElement(el) { 147 if (!el) return; 148 149 if (this.reduceMotion) { 150 this.revealElement(el); 151 return; 152 } 153 if (this.observer) this.observer.observe(el); 154 155 if (!this.elements.includes(el)) { 156 this.elements = [...this.elements, el]; 157 } 158 } 159 160 reset() {
161 this.elements.forEach((el) => this.resetElement(el)); 162 this.checkElements(); 163 } 164 165 isWipe(el) { 166 return ( 167 el.classList.contains("scroll-wipe") || 168 el.classList.contains("scroll-wipe-left") || 169 el.classList.contains("scroll-wipe-right") || 170 el.classList.contains("scroll-wipe-top") 171 ); 172 } 173} 174 175/* ------------------------------------------------------- 176 Utilidades opcionales 177------------------------------------------------------- */ 178 179// HeurÃstica ligera para auto-etiquetar (desactivada por defecto) 180function autoAddScrollReveal() { 181 const sets = [ 182 { selector: "h1, h2, h3, h4, h5, h6", cls: "scroll-reveal-title" }, 183 { selector: ".card, .product, .service", cls: "scroll-reveal-card" }, 184 { selector: "img", cls: "scroll-reveal-image" }, 185 { selector: "p:not(.no-reveal), .content-section", cls: "scroll-fade" }, 186 ]; 187 188 sets.forEach(({ selector, cls }) => { 189 document.querySelectorAll(selector).forEach((el) => { 190 if ( 191 el.classList.contains("scroll-reveal") || 192 el.classList.contains("scroll-reveal-left") || 193 el.classList.contains("scroll-reveal-right") || 194 el.classList.contains("scroll-reveal-scale") || 195 el.classList.contains("scroll-reveal-title") || 196 el.classList.contains("scroll-reveal-card") || 197 el.classList.contains("scroll-reveal-image") || 198 el.classList.contains("scroll-reveal-fade") || 199 el.classList.contains("scroll-fade") || 200 el.classList.contains("scroll-fade-children") || 201 el.classList.contains("scroll-wipe") || 202 el.classList.contains("scroll-wipe-left") || 203 el.classList.contains("scroll-wipe-right") || 204 el.classList.contains("scroll-wipe-top") || 205 el.classList.contains("scroll-zoom") || 206 el.classList.contains("scroll-zoom-children") 207 ) 208 return; 209 210 el.classList.add(cls); 211 }); 212 }); 213} 214 215// Inicialización 216document.addEventListener("DOMContentLoaded", () => { 217 // autoAddScrollReveal(); // <- activa si quieres auto-etiquetar 218 const scrollReveal = new ScrollReveal({ 219 threshold: 0.15, 220 reanimate: true, // false si solo quieres animar una vez 221 rootMargin: "0px 0px -50px 0px", 222 forceMotion: false, // true para ignorar prefers-reduced-motion (debug) 223 }); 224 window.scrollReveal = scrollReveal; 225}); 226 227// API manual: agrega clase y registra en el observer 228function addScrollReveal(selector, animationType = "scroll-fade") { 229 document.querySelectorAll(selector).forEach((el) => { 230 el.classList.add(animationType); 231 if (window.scrollReveal) window.scrollReveal.addElement(el); 232 }); 233} 234 235// CommonJS export 236if (typeof module !== "undefined" && module.exports) { 237 module.exports = { ScrollReveal, addScrollReveal, autoAddScrollReveal }; 238}
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.