1/** 2 * LU Smooth Scroll 3 * High-performance, vanilla JavaScript smooth scrolling with requestAnimationFrame. 4 */ 5const luSmoothScroll = (function () { 6 let settings = { 7 duration: 1200, 8 easing: 'easeOutQuint' 9 }; 10 11 if (typeof luSmoothScrollSettings !== 'undefined') { 12 settings.duration = parseFloat(luSmoothScrollSettings.duration) * 1000; 13 settings.easing = luSmoothScrollSettings.easing || 'easeOutQuint'; 14 } 15 16 const easings = { 17 linear: t => t, 18 easeInQuad: t => t * t, 19 easeOutQuad: t => t * (2 - t), 20 easeInOutQuad: t => t < .5 ? 2 * t * t : -1 + (4 - 2 * t) * t, 21 easeInCubic: t => t * t * t, 22 easeOutCubic: t => (--t) * t * t + 1, 23 easeInOutCubic: t => t < .5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1, 24 easeInQuart: t => t * t * t * t, 25 easeOutQuart: t => 1 - (--t) * t * t * t, 26 easeInOutQuart: t => t < .5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t, 27 easeInQuint: t => t * t * t * t * t, 28 easeOutQuint: t => 1 + (--t) * t * t * t * t, 29 easeInOutQuint: t => t < .5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t 30 }; 31 32 let baseY = 0; 33 let animations = []; 34 let isAnimating = false; 35 let animationFrameId = null; 36 let lastRenderedY = -1; 37 38 /** 39 * Checks if the user is on a mobile device or touch environment. 40 * @returns {boolean} True if mobile, false otherwise. 41 */ 42 function isMobile() { 43 return window.innerWidth < 768 || ('ontouchstart' in window) || navigator.maxTouchPoints > 0; 44 } 45 46 /** 47 * Determines if a DOM element or its ancestors have their own scrollbar. 48 * @param {HTMLElement} node The target node. 49 * @returns {boolean} True if a scrollable parent exists. 50 */ 51 function hasScrollableParent(node) { 52 let parent = node; 53 while (parent && parent !== document.body && parent !== document.documentElement) { 54 const style = window.getComputedStyle(parent); 55 if (['auto', 'scroll'].includes(style.overflowY)) { 56 if (parent.scrollHeight > parent.clientHeight) { 57 return true; 58 } 59 } 60 parent = parent.parentNode; 61 } 62 return false; 63 } 64 65 /** 66 * The main animation loop executed via requestAnimationFrame. 67 * @param {number} timestamp The current time elapsed. 68 */ 69 function scrollLoop(timestamp) { 70 if (Math.abs(window.scrollY - lastRenderedY) > 5) { 71 isAnimating = false; 72 animationFrameId = null; 73 animations = []; 74 baseY = window.scrollY; 75 return; 76 } 77 78 let totalOffset = 0; 79 let stillAnimating = []; 80 81 for (let i = 0; i < animations.length; i++) { 82 let anim = animations[i]; 83 if (!anim.startTime) { 84 anim.startTime = timestamp; 85 } 86 87 const elapsed = timestamp - anim.startTime; 88 let progress = Math.min(elapsed / settings.duration, 1); 89 90 if (progress < 1) { 91 let easingFunc = easings[settings.easing] || easings['easeOutQuint']; 92 const easedProgress = easingFunc(progress); 93 totalOffset += anim.delta * easedProgress; 94 stillAnimating.push(anim); 95 } else { 96 baseY += anim.delta; 97 } 98 } 99 100 animations = stillAnimating; 101 102 let nextY = baseY + totalOffset; 103 104 const maxScroll = Math.max(0, document.documentElement.scrollHeight - window.innerHeight); 105 nextY = Math.max(0, Math.min(nextY, maxScroll)); 106 107 window.scrollTo(0, nextY); 108 109 lastRenderedY = window.scrollY; 110 111 if (animations.length > 0) { 112 animationFrameId = requestAnimationFrame(scrollLoop); 113 } else { 114 isAnimating = false; 115 animationFrameId = null; 116 baseY = window.scrollY; 117 } 118 } 119 120 /** 121 * Updates the target scroll destination. 122 * @param {number} deltaY The scroll offset. 123 * @returns {boolean} True if the target was updated and animation triggered. 124 */ 125 function updateTarget(deltaY) { 126 if (isMobile()) return false;
127 128 const maxScroll = Math.max(0, document.documentElement.scrollHeight - window.innerHeight); 129 130 if (!isAnimating) { 131 baseY = window.scrollY; 132 lastRenderedY = window.scrollY; 133 animations = []; 134 } 135 136 let currentTarget = baseY; 137 for (let i = 0; i < animations.length; i++) { 138 currentTarget += animations[i].delta; 139 } 140 141 let newTarget = currentTarget + deltaY; 142 newTarget = Math.max(0, Math.min(newTarget, maxScroll)); 143 144 let clampedDelta = newTarget - currentTarget; 145 146 if (clampedDelta !== 0) { 147 animations.push({ 148 delta: clampedDelta, 149 startTime: null 150 }); 151 152 if (!isAnimating) { 153 isAnimating = true; 154 animationFrameId = requestAnimationFrame(scrollLoop); 155 } 156 return true; 157 } 158 return false; 159 } 160 161 /** 162 * Handles the native wheel event. 163 * @param {WheelEvent} e The triggered event. 164 */ 165 function onWheel(e) { 166 if (isMobile()) return; 167 168 if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) return; 169 170 if (hasScrollableParent(e.target)) return; 171 172 e.preventDefault(); 173 174 let deltaY = e.deltaY; 175 if (e.deltaMode === 1) deltaY *= 40; 176 else if (e.deltaMode === 2) deltaY *= window.innerHeight; 177 178 updateTarget(deltaY); 179 } 180 181 /** 182 * Handles keyboard events for page navigation. 183 * @param {KeyboardEvent} e The triggered event. 184 */ 185 function onKeyDown(e) { 186 if (isMobile()) return; 187 188 const activeEl = document.activeElement; 189 if (activeEl && (['INPUT', 'TEXTAREA', 'SELECT'].includes(activeEl.tagName) || activeEl.isContentEditable)) { 190 return; 191 } 192 193 let deltaY = 0; 194 const vh = window.innerHeight; 195 196 switch (e.keyCode) { 197 case 33: 198 deltaY = -vh + 40; 199 break; 200 case 34: 201 deltaY = vh - 40; 202 break; 203 case 38: 204 deltaY = -60; 205 break; 206 case 40: 207 deltaY = 60; 208 break; 209 case 32: 210 deltaY = e.shiftKey ? -vh + 40 : vh - 40; 211 break; 212 default: 213 return; 214 } 215 216 if (updateTarget(deltaY)) { 217 e.preventDefault(); 218 } 219 } 220 221 /** 222 * Instantly aborts the currently running scroll animation. 223 */ 224 function stopAnimation() { 225 if (isAnimating) { 226 cancelAnimationFrame(animationFrameId); 227 isAnimating = false; 228 animationFrameId = null; 229 animations = []; 230 baseY = window.scrollY; 231 lastRenderedY = window.scrollY; 232 } 233 } 234 235 /** 236 * Cancels the animation if an anchor link is clicked. 237 * @param {MouseEvent} e The triggered event. 238 */ 239 function onHashClick(e) { 240 const target = e.target.closest('a'); 241 if (target && target.hash && target.hash.startsWith('#')) { 242 stopAnimation(); 243 setTimeout(() => { 244 baseY = window.scrollY; 245 lastRenderedY = window.scrollY; 246 }, 50); 247 } 248 } 249 250 /** 251 * Syncs the target position upon passive native scroll. 252 */ 253 function onPassiveScroll() { 254 if (!isAnimating) { 255 baseY = window.scrollY; 256 } 257 } 258 259 /** 260 * Initializes the smooth scroll functionality by adding event listeners. 261 */ 262 function init() { 263 if (isMobile()) return; 264 265 baseY = window.scrollY; 266 267 window.addEventListener('wheel', onWheel, { passive: false }); 268 window.addEventListener('keydown', onKeyDown, { passive: false }); 269 270 document.addEventListener('click', onHashClick); 271 window.addEventListener('scroll', onPassiveScroll, { passive: true }); 272 window.addEventListener('resize', onPassiveScroll, { passive: true }); 273 274 window.addEventListener('mousedown', stopAnimation); 275 window.addEventListener('touchstart', stopAnimation, { passive: true }); 276 } 277 278 return { 279 init: init 280 }; 281})(); 282 283if (document.readyState === 'loading') { 284 document.addEventListener('DOMContentLoaded', luSmoothScroll.init); 285} else { 286 luSmoothScroll.init(); 287}
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.