From ed7b8fc416ccc3519586dd5906506dff6009a24a Mon Sep 17 00:00:00 2001 From: Alexey Grekov Date: Sun, 16 Nov 2025 02:07:21 -0700 Subject: [PATCH] Smooth scrolling copied from tsu-website pull requst for more information go there --- src/pages/_app.tsx | 1 + src/utility/smoothscroll.js | 812 ++++++++++++++++++++++++++++++++++++ 2 files changed, 813 insertions(+) create mode 100644 src/utility/smoothscroll.js diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index c9628cd..695b5a5 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -15,6 +15,7 @@ const inter = Inter({ subsets: ["latin"] }); export default function App({ Component, pageProps }: AppProps) { useEffect(() => { + import("../utility/smoothscroll").catch(() => {}); Aos.init({ duration: 1000 }); // Init AOS. Sets AOS default duration as 1s }, []); diff --git a/src/utility/smoothscroll.js b/src/utility/smoothscroll.js new file mode 100644 index 0000000..497c477 --- /dev/null +++ b/src/utility/smoothscroll.js @@ -0,0 +1,812 @@ +/* eslint-disable */ +/*! + * SmoothScroll for websites v1.4.9 (Balazs Galambosi) + * http://www.smoothscroll.net/ + * + * Licensed under the terms of the MIT license. + * + * You may use it in your theme if you credit me. + * It is also free to use on any individual website. + * + * Exception: + * The only restriction is to not publish any + * extension for browsers or native application + * without getting a written permission first. + */ + +(function () { + // Scroll Variables (tweakable) + var defaultOptions = { + // Scrolling Core + frameRate: 150, // [Hz] + animationTime: 400, // [ms] + stepSize: 100, // [px] + + // Pulse (less tweakable) + // ratio of "tail" to "acceleration" + pulseAlgorithm: true, + pulseScale: 4, + pulseNormalize: 1, + + // Acceleration + accelerationDelta: 50, // 50 + accelerationMax: 3, // 3 + + // Keyboard Settings + keyboardSupport: true, // option + arrowScroll: 50, // [px] + + // Other + fixedBackground: true, + excluded: "", + }; + + var options = defaultOptions; + + // Other Variables + var isExcluded = false; + var isFrame = false; + var direction = { x: 0, y: 0 }; + var initDone = false; + var root = document.documentElement; + var activeElement; + var observer; + var refreshSize; + var deltaBuffer = []; + var deltaBufferTimer; + var isMac = /^Mac/.test(navigator.platform); + + var key = { + left: 37, + up: 38, + right: 39, + down: 40, + spacebar: 32, + pageup: 33, + pagedown: 34, + end: 35, + home: 36, + }; + var arrowKeys = { 37: 1, 38: 1, 39: 1, 40: 1 }; + + /*********************************************** + * INITIALIZE + ***********************************************/ + + /** + * Tests if smooth scrolling is allowed. Shuts down everything if not. + */ + function initTest() { + if (options.keyboardSupport) { + addEvent("keydown", keydown); + } + } + + /** + * Sets up scrolls array, determines if frames are involved. + */ + function init() { + if (initDone || !document.body) return; + + initDone = true; + + var body = document.body; + var html = document.documentElement; + var windowHeight = window.innerHeight; + var scrollHeight = body.scrollHeight; + + // check compat mode for root element + root = document.compatMode.indexOf("CSS") >= 0 ? html : body; + activeElement = body; + + initTest(); + + // Checks if this script is running in a frame + if (top != self) { + isFrame = true; + } + + /** + * Safari 10 fixed it, Chrome fixed it in v45: + * This fixes a bug where the areas left and right to + * the content does not trigger the onmousewheel event + * on some pages. e.g.: html, body { height: 100% } + */ + else if ( + isOldSafari && + scrollHeight > windowHeight && + (body.offsetHeight <= windowHeight || html.offsetHeight <= windowHeight) + ) { + var fullPageElem = document.createElement("div"); + fullPageElem.style.cssText = + "position:absolute; z-index:-10000; " + + "top:0; left:0; right:0; height:" + + root.scrollHeight + + "px"; + document.body.appendChild(fullPageElem); + + // DOM changed (throttled) to fix height + var pendingRefresh; + refreshSize = function () { + if (pendingRefresh) return; // could also be: clearTimeout(pendingRefresh); + pendingRefresh = setTimeout(function () { + if (isExcluded) return; // could be running after cleanup + fullPageElem.style.height = "0"; + fullPageElem.style.height = root.scrollHeight + "px"; + pendingRefresh = null; + }, 500); // act rarely to stay fast + }; + + setTimeout(refreshSize, 10); + + addEvent("resize", refreshSize); + + // TODO: attributeFilter? + var config = { + attributes: true, + childList: true, + characterData: false, + // subtree: true + }; + + observer = new MutationObserver(refreshSize); + observer.observe(body, config); + + if (root.offsetHeight <= windowHeight) { + var clearfix = document.createElement("div"); + clearfix.style.clear = "both"; + body.appendChild(clearfix); + } + } + + // disable fixed background + if (!options.fixedBackground && !isExcluded) { + body.style.backgroundAttachment = "scroll"; + html.style.backgroundAttachment = "scroll"; + } + } + + /** + * Removes event listeners and other traces left on the page. + */ + function cleanup() { + observer && observer.disconnect(); + removeEvent(wheelEvent, wheel); + removeEvent("mousedown", mousedown); + removeEvent("keydown", keydown); + removeEvent("resize", refreshSize); + removeEvent("load", init); + } + + /************************************************ + * SCROLLING + ************************************************/ + + var que = []; + var pending = false; + var lastScroll = Date.now(); + + /** + * Pushes scroll actions to the scrolling queue. + */ + function scrollArray(elem, left, top) { + directionCheck(left, top); + + if (options.accelerationMax != 1) { + var now = Date.now(); + var elapsed = now - lastScroll; + if (elapsed < options.accelerationDelta) { + var factor = (1 + 50 / elapsed) / 2; + if (factor > 1) { + factor = Math.min(factor, options.accelerationMax); + left *= factor; + top *= factor; + } + } + lastScroll = Date.now(); + } + + // push a scroll command + que.push({ + x: left, + y: top, + lastX: left < 0 ? 0.99 : -0.99, + lastY: top < 0 ? 0.99 : -0.99, + start: Date.now(), + }); + + // don't act if there's a pending queue + if (pending) { + return; + } + + var scrollRoot = getScrollRoot(); + var isWindowScroll = elem === scrollRoot || elem === document.body; + + // if we haven't already fixed the behavior, + // and it needs fixing for this sesh + if (elem.$scrollBehavior == null && isScrollBehaviorSmooth(elem)) { + elem.$scrollBehavior = elem.style.scrollBehavior; + elem.style.scrollBehavior = "auto"; + } + + var step = function () { + var now = Date.now(); + var scrollX = 0; + var scrollY = 0; + + for (var i = 0; i < que.length; i++) { + var item = que[i]; + var elapsed = now - item.start; + var finished = elapsed >= options.animationTime; + + // scroll position: [0, 1] + var position = finished ? 1 : elapsed / options.animationTime; + + // easing [optional] + if (options.pulseAlgorithm) { + position = pulse(position); + } + + // only need the difference + var x = ((item.x * position - item.lastX) >> 0); + var y = ((item.y * position - item.lastY) >> 0); + + // add this to the total scrolling + scrollX += x; + scrollY += y; + + // update last values + item.lastX += x; + item.lastY += y; + + // delete and step back if it's over + if (finished) { + que.splice(i, 1); + i--; + } + } + + // scroll left and top + if (isWindowScroll) { + window.scrollBy(scrollX, scrollY); + } else { + if (scrollX) elem.scrollLeft += scrollX; + if (scrollY) elem.scrollTop += scrollY; + } + + // clean up if there's nothing left to do + if (!left && !top) { + que = []; + } + + if (que.length) { + requestFrame(step, elem, 1000 / options.frameRate + 1); + } else { + pending = false; + // restore default behavior at the end of scrolling sesh + if (elem.$scrollBehavior != null) { + elem.style.scrollBehavior = elem.$scrollBehavior; + elem.$scrollBehavior = null; + } + } + }; + + // start a new queue of actions + requestFrame(step, elem, 0); + pending = true; + } + + /*********************************************** + * EVENTS + ***********************************************/ + + /** + * Mouse wheel handler. + * @param {Object} event + */ + function wheel(event) { + if (!initDone) { + init(); + } + + var target = event.target; + + // leave early if default action is prevented + // or it's a zooming event with CTRL + if (event.defaultPrevented || event.ctrlKey) { + return true; + } + + // leave embedded content alone (flash & pdf) + if ( + isNodeName(activeElement, "embed") || + (isNodeName(target, "embed") && /\.pdf/i.test(target.src)) || + isNodeName(activeElement, "object") || + target.shadowRoot + ) { + return true; + } + + var deltaX = -event.wheelDeltaX || event.deltaX || 0; + var deltaY = -event.wheelDeltaY || event.deltaY || 0; + + if (isMac) { + if (event.wheelDeltaX && isDivisible(event.wheelDeltaX, 120)) { + deltaX = -120 * (event.wheelDeltaX / Math.abs(event.wheelDeltaX)); + } + if (event.wheelDeltaY && isDivisible(event.wheelDeltaY, 120)) { + deltaY = -120 * (event.wheelDeltaY / Math.abs(event.wheelDeltaY)); + } + } + + // use wheelDelta if deltaX/Y is not available + if (!deltaX && !deltaY) { + deltaY = -event.wheelDelta || 0; + } + + // line based scrolling (Firefox mostly) + if (event.deltaMode === 1) { + deltaX *= 40; + deltaY *= 40; + } + + var overflowing = overflowingAncestor(target); + + // nothing to do if there's no element that's scrollable + if (!overflowing) { + // except Chrome iframes seem to eat wheel events, which we need to + // propagate up, if the iframe has nothing overflowing to scroll + if (isFrame && isChrome) { + // change target to iframe element itself for the parent frame + Object.defineProperty(event, "target", { value: window.frameElement }); + return parent.wheel(event); + } + return true; + } + + // check if it's a touchpad scroll that should be ignored + if (isTouchpad(deltaY)) { + return true; + } + + // scale by step size + // delta is 120 most of the time + // synaptics seems to send 1 sometimes + if (Math.abs(deltaX) > 1.2) { + deltaX *= options.stepSize / 120; + } + if (Math.abs(deltaY) > 1.2) { + deltaY *= options.stepSize / 120; + } + + scrollArray(overflowing, deltaX, deltaY); + event.preventDefault(); + scheduleClearCache(); + } + + /** + * Keydown event handler. + * @param {Object} event + */ + function keydown(event) { + var target = event.target; + var modifier = + event.ctrlKey || + event.altKey || + event.metaKey || + (event.shiftKey && event.keyCode !== key.spacebar); + + // our own tracked active element could've been removed from the DOM + if (!document.body.contains(activeElement)) { + activeElement = document.activeElement; + } + + // do nothing if user is editing text + // or using a modifier key (except shift) + // or in a dropdown + // or inside interactive elements + var inputNodeNames = /^(textarea|select|embed|object)$/i; + var buttonTypes = /^(button|submit|radio|checkbox|file|color|image)$/i; + if ( + event.defaultPrevented || + inputNodeNames.test(target.nodeName) || + (isNodeName(target, "input") && !buttonTypes.test(target.type)) || + isNodeName(activeElement, "video") || + isInsideYoutubeVideo(event) || + target.isContentEditable || + modifier + ) { + return true; + } + + // [spacebar] should trigger button press, leave it alone + if ( + (isNodeName(target, "button") || + (isNodeName(target, "input") && buttonTypes.test(target.type))) && + event.keyCode === key.spacebar + ) { + return true; + } + + // [arrow keys] on radio buttons should be left alone + if ( + isNodeName(target, "input") && + target.type == "radio" && + arrowKeys[event.keyCode] + ) { + return true; + } + + var shift, + x = 0, + y = 0; + var overflowing = overflowingAncestor(activeElement); + + if (!overflowing) { + // Chrome iframes seem to eat key events, which we need to + // propagate up, if the iframe has nothing overflowing to scroll + return isFrame && isChrome ? parent.keydown(event) : true; + } + + var clientHeight = overflowing.clientHeight; + + if (overflowing == document.body) { + clientHeight = window.innerHeight; + } + + switch (event.keyCode) { + case key.up: + y = -options.arrowScroll; + break; + case key.down: + y = options.arrowScroll; + break; + case key.spacebar: // (+ shift) + shift = event.shiftKey ? 1 : -1; + y = -shift * clientHeight * 0.9; + break; + case key.pageup: + y = -clientHeight * 0.9; + break; + case key.pagedown: + y = clientHeight * 0.9; + break; + case key.home: + if (overflowing == document.body && document.scrollingElement) + overflowing = document.scrollingElement; + y = -overflowing.scrollTop; + break; + case key.end: + var scroll = overflowing.scrollHeight - overflowing.scrollTop; + var scrollRemaining = scroll - clientHeight; + y = scrollRemaining > 0 ? scrollRemaining + 10 : 0; + break; + case key.left: + x = -options.arrowScroll; + break; + case key.right: + x = options.arrowScroll; + break; + default: + return true; // a key we don't care about + } + + scrollArray(overflowing, x, y); + event.preventDefault(); + scheduleClearCache(); + } + + /** + * Mousedown event only for updating activeElement + */ + function mousedown(event) { + activeElement = event.target; + } + + /*********************************************** + * OVERFLOW + ***********************************************/ + + var uniqueID = (function () { + var i = 0; + return function (el) { + return el.uniqueID || (el.uniqueID = i++); + }; + })(); + + var cacheX = {}; // cleared out after a scrolling session + var cacheY = {}; // cleared out after a scrolling session + var clearCacheTimer; + var smoothBehaviorForElement = {}; + + function scheduleClearCache() { + clearTimeout(clearCacheTimer); + clearCacheTimer = setInterval(function () { + cacheX = cacheY = smoothBehaviorForElement = {}; + }, 1 * 1000); + } + + function setCache(elems, overflowing, x) { + var cache = x ? cacheX : cacheY; + for (var i = elems.length; i--; ) cache[uniqueID(elems[i])] = overflowing; + return overflowing; + } + + function getCache(el, x) { + return (x ? cacheX : cacheY)[uniqueID(el)]; + } + + // (body) (root) + // | hidden | visible | scroll | auto | + // hidden | no | no | YES | YES | + // visible | no | YES | YES | YES | + // scroll | no | YES | YES | YES | + // auto | no | YES | YES | YES | + + function overflowingAncestor(el) { + var elems = []; + var body = document.body; + var rootScrollHeight = root.scrollHeight; + do { + var cached = getCache(el, false); + if (cached) { + return setCache(elems, cached); + } + elems.push(el); + if (rootScrollHeight === el.scrollHeight) { + var topOverflowsNotHidden = + overflowNotHidden(root) && overflowNotHidden(body); + var isOverflowCSS = + topOverflowsNotHidden || overflowAutoOrScroll(root); + if ( + (isFrame && isContentOverflowing(root)) || + (!isFrame && isOverflowCSS) + ) { + return setCache(elems, getScrollRoot()); + } + } else if (isContentOverflowing(el) && overflowAutoOrScroll(el)) { + return setCache(elems, el); + } + } while ((el = el.parentElement)); + } + + function isContentOverflowing(el) { + return el.clientHeight + 10 < el.scrollHeight; + } + + // typically for and + function overflowNotHidden(el) { + var overflow = getComputedStyle(el, "").getPropertyValue("overflow-y"); + return overflow !== "hidden"; + } + + // for all other elements + function overflowAutoOrScroll(el) { + var overflow = getComputedStyle(el, "").getPropertyValue("overflow-y"); + return overflow === "scroll" || overflow === "auto"; + } + + // for all other elements + function isScrollBehaviorSmooth(el) { + var id = uniqueID(el); + if (smoothBehaviorForElement[id] == null) { + var scrollBehavior = getComputedStyle(el, "")["scroll-behavior"]; + smoothBehaviorForElement[id] = "smooth" == scrollBehavior; + } + return smoothBehaviorForElement[id]; + } + + /*********************************************** + * HELPERS + ***********************************************/ + + function addEvent(type, fn, arg) { + window.addEventListener(type, fn, arg || false); + } + + function removeEvent(type, fn, arg) { + window.removeEventListener(type, fn, arg || false); + } + + function isNodeName(el, tag) { + return (el && (el.nodeName || "").toLowerCase()) === tag.toLowerCase(); + } + + function directionCheck(x, y) { + x = x > 0 ? 1 : -1; + y = y > 0 ? 1 : -1; + if (direction.x !== x || direction.y !== y) { + direction.x = x; + direction.y = y; + que = []; + lastScroll = 0; + } + } + + if (window.localStorage && localStorage.SS_deltaBuffer) { + try { + // #46 Safari throws in private browsing for localStorage + deltaBuffer = localStorage.SS_deltaBuffer.split(","); + } catch (e) {} + } + + function isTouchpad(deltaY) { + if (!deltaY) return; + if (!deltaBuffer.length) { + deltaBuffer = [deltaY, deltaY, deltaY]; + } + deltaY = Math.abs(deltaY); + deltaBuffer.push(deltaY); + deltaBuffer.shift(); + clearTimeout(deltaBufferTimer); + deltaBufferTimer = setTimeout(function () { + try { + // #46 Safari throws in private browsing for localStorage + localStorage.SS_deltaBuffer = deltaBuffer.join(","); + } catch (e) {} + }, 1000); + var dpiScaledWheelDelta = + deltaY > 120 && allDeltasDivisableBy(deltaY); // win64 + return ( + !allDeltasDivisableBy(120) && + !allDeltasDivisableBy(100) && + !dpiScaledWheelDelta + ); + } + + function isDivisible(n, divisor) { + return Math.floor(n / divisor) == n / divisor; + } + + function allDeltasDivisableBy(divisor) { + return ( + isDivisible(deltaBuffer[0], divisor) && + isDivisible(deltaBuffer[1], divisor) && + isDivisible(deltaBuffer[2], divisor) + ); + } + + function isInsideYoutubeVideo(event) { + var elem = event.target; + var isControl = false; + if (document.URL.indexOf("www.youtube.com/watch") != -1) { + do { + isControl = + elem.classList && elem.classList.contains("html5-video-controls"); + if (isControl) break; + } while ((elem = elem.parentNode)); + } + return isControl; + } + + var requestFrame = (function () { + return ( + window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + function (callback, element, delay) { + window.setTimeout(callback, delay || 1000 / 60); + } + ); + })(); + + var MutationObserver = + window.MutationObserver || + window.WebKitMutationObserver || + window.MozMutationObserver; + + var getScrollRoot = (function () { + var SCROLL_ROOT = document.scrollingElement; + return function () { + if (!SCROLL_ROOT) { + var dummy = document.createElement("div"); + dummy.style.cssText = "height:10000px;width:1px;"; + document.body.appendChild(dummy); + var bodyScrollTop = document.body.scrollTop; + var docElScrollTop = document.documentElement.scrollTop; + window.scrollBy(0, 3); + if (document.body.scrollTop != bodyScrollTop) SCROLL_ROOT = document.body; + else SCROLL_ROOT = document.documentElement; + window.scrollBy(0, -3); + document.body.removeChild(dummy); + } + return SCROLL_ROOT; + }; + })(); + + /*********************************************** + * PULSE (by Michael Herf) + ***********************************************/ + + /** + * Viscous fluid with a pulse for part and decay for the rest. + * - Applies a fixed force over an interval (a damped acceleration), and + * - Lets the exponential bleed away the velocity over a longer interval + * - Michael Herf, http://stereopsis.com/stopping/ + */ + function pulse_(x) { + var val, start, expx; + // test + x = x * options.pulseScale; + if (x < 1) { + // acceleartion + val = x - (1 - Math.exp(-x)); + } else { + // tail + // the previous animation ended here: + start = Math.exp(-1); + // simple viscous drag + x -= 1; + expx = 1 - Math.exp(-x); + val = start + expx * (1 - start); + } + return val * options.pulseNormalize; + } + + function pulse(x) { + if (x >= 1) return 1; + if (x <= 0) return 0; + + if (options.pulseNormalize == 1) { + options.pulseNormalize /= pulse_(1); + } + return pulse_(x); + } + + /*********************************************** + * FIRST RUN + ***********************************************/ + + var userAgent = window.navigator.userAgent; + var isEdge = /Edge/.test(userAgent); // thank you MS + var isChrome = /chrome/i.test(userAgent) && !isEdge; + var isSafari = /safari/i.test(userAgent) && !isEdge; + var isMobile = /mobile/i.test(userAgent); + var isIEWin7 = /Windows NT 6.1/i.test(userAgent) && /rv:11/i.test(userAgent); + var isOldSafari = + isSafari && (/Version\/8/i.test(userAgent) || /Version\/9/i.test(userAgent)); + var isEnabledForBrowser = (isChrome || isSafari || isIEWin7) && !isMobile; + + var supportsPassive = false; + try { + window.addEventListener( + "test", + null, + Object.defineProperty({}, "passive", { + get: function () { + supportsPassive = true; + }, + }) + ); + } catch (e) {} + + var wheelOpt = supportsPassive ? { passive: false } : false; + var wheelEvent = + "onwheel" in document.createElement("div") ? "wheel" : "mousewheel"; + + if (wheelEvent && isEnabledForBrowser) { + addEvent(wheelEvent, wheel, wheelOpt); + addEvent("mousedown", mousedown); + addEvent("load", init); + } + + /*********************************************** + * PUBLIC INTERFACE + ***********************************************/ + + function SmoothScroll(optionsToSet) { + for (var key in optionsToSet) + if (defaultOptions.hasOwnProperty(key)) options[key] = optionsToSet[key]; + } + SmoothScroll.destroy = cleanup; + + if (window.SmoothScrollOptions) SmoothScroll(window.SmoothScrollOptions); + + if (typeof define === "function" && define.amd) + define(function () { + return SmoothScroll; + }); + else if ("object" == typeof exports) module.exports = SmoothScroll; + else window.SmoothScroll = SmoothScroll; +})();