Smooth Scroll

Configurable smooth scroll with optional motion blur. Uses requestAnimationFrame (like V-Sync).

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Smooth Scroll
// @description  Configurable smooth scroll with optional motion blur. Uses requestAnimationFrame (like V-Sync).
// @author       DARK1E
// @icon         https://i.imgur.com/IAwk6NN.png
// @include      *
// @version      3.3
// @namespace    sttb-dxrk1e
// @license      MIT
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';

    const cfg = {
        smth: 0.85,
        stpMult: 1,
        accDelFct: 0.2,
        accMaxMult: 3,
        thrsh: 1,
        lnHt: 20,
        mBlur: false,
        mBlurInt: 0.3,
        dbg: false,
    };

    const stMap = new WeakMap();
    const DAMP_FCT = 1 - cfg.smth;
    const ACC_TMO = 150;
    const MAX_BLUR = 5;
    const BLUR_THRESH = 0.2;

    function _animStep(el) {
        const st = stMap.get(el);
        if (!st) return;

        const curScrTop = _getScrTop(el);
        const delta = st.tgtY - st.curY;

        if (Math.abs(delta) < cfg.thrsh && Math.abs(curScrTop - st.tgtY) < cfg.thrsh) {
            if (cfg.dbg) console.log("SS: Anim end", el);
            if (Math.abs(curScrTop - st.tgtY) > 0.1) {
                 _setScrTop(el, Math.round(st.tgtY));
            }
            _cancelAnim(el);
            return;
        }

        const step = delta * DAMP_FCT;
        st.curY += step;

        const scrAmt = Math.round(st.curY) - curScrTop;

        if (scrAmt !== 0) {
            const origBehav = _setBehav(el, 'auto');
            _setScrTop(el, curScrTop + scrAmt);
        }

        if (cfg.mBlur) {
            const blurPx = Math.min(MAX_BLUR, Math.abs(step) * cfg.mBlurInt);
            if (blurPx > BLUR_THRESH) {
                _setFilter(el, `blur(${blurPx.toFixed(1)}px)`);
            } else {
                _setFilter(el, 'none');
            }
        }

        st.animId = requestAnimationFrame(() => _animStep(el));
    }

    function _startOrUpd(el, dY) {
        let st = stMap.get(el);
        const now = performance.now();

        if (!st) {
            st = {
                tgtY: _getScrTop(el),
                curY: _getScrTop(el),
                animId: null,
                ts: 0,
                mult: 1,
            };
            stMap.set(el, st);
        }

        const dt = now - st.ts;
        if (dt < ACC_TMO) {
            const accInc = Math.abs(dY) * cfg.accDelFct / cfg.lnHt;
            st.mult = Math.min(cfg.accMaxMult, st.mult + accInc);
        } else {
            st.mult = 1;
        }
        st.ts = now;

        const effDel = dY * st.mult * cfg.stpMult;
        st.tgtY += effDel;
        st.tgtY = _clampTgt(el, st.tgtY);

        if (cfg.dbg) {
            console.log(`SS: Upd Tgt`, el, `| dY: ${dY.toFixed(2)}`, `| mult: ${st.mult.toFixed(2)}`, `| effDel: ${effDel.toFixed(2)}`, `| tgtY: ${st.tgtY.toFixed(2)}`);
        }

        if (!st.animId) {
            st.curY = _getScrTop(el);
            if (cfg.dbg) console.log("SS: Start anim", el);
            st.animId = requestAnimationFrame(() => _animStep(el));
        }
    }

    function _cancelAnim(el) {
        const st = stMap.get(el);
        if (st?.animId) {
            cancelAnimationFrame(st.animId);
            stMap.delete(el);
             if (cfg.dbg) console.log("SS: Anim cancelled", el);
        }
        if (cfg.mBlur) {
             _setFilter(el, 'none');
        }
    }

    function _getScrTop(el) {
        return (el === window) ? (window.scrollY || document.documentElement.scrollTop) : /** @type {Element} */ (el).scrollTop;
    }

    function _setScrTop(el, val) {
        if (el === window) {
            document.documentElement.scrollTop = val;
        } else {
            /** @type {Element} */ (el).scrollTop = val;
        }
    }

    function _setBehav(el, behav) {
        const target = (el === window) ? document.documentElement : el;
        if (target instanceof Element) {
            const orig = target.style.scrollBehavior;
            target.style.scrollBehavior = behav;
            return orig;
        }
        return undefined;
    }

    function _setFilter(el, val) {
         const target = (el === window) ? document.documentElement : el;
         if (target instanceof HTMLElement) {
             try {
                target.style.filter = val;
             } catch (e) {
                 if (cfg.dbg) console.warn("SS: Failed to set filter on", target, e);
             }
         }
    }

    function _clampTgt(el, tgtY) {
        let maxScr;
        if (el === window) {
            maxScr = document.documentElement.scrollHeight - window.innerHeight;
        } else {
            const htmlEl = /** @type {Element} */ (el);
            maxScr = htmlEl.scrollHeight - htmlEl.clientHeight;
        }
        return Math.max(0, Math.min(tgtY, maxScr));
    }

    function _isScr(el) {
        if (!el || !(el instanceof Element) || el === document.documentElement || el === document.body) {
            return false;
        }
        try {
            const style = window.getComputedStyle(el);
            const ovf = style.overflowY;
            const isOvf = ovf === 'scroll' || ovf === 'auto';
            const canScr = el.scrollHeight > el.clientHeight + 1;
            return isOvf && canScr;
        } catch (e) {
            if (cfg.dbg) console.warn("SS: Err check scroll", el, e);
            return false;
        }
    }

    function _getTgt(e) {
        const path = e.composedPath ? e.composedPath() : [];

        for (const el of path) {
            if (!(el instanceof Element)) continue;

            if (_isScr(el)) {
                const curScr = _getScrTop(el);
                const maxScr = el.scrollHeight - el.clientHeight;
                if ((e.deltaY < 0 && curScr > 0.1) || (e.deltaY > 0 && curScr < maxScr - 0.1)) {
                     if (cfg.dbg) console.log("SS: Found el in path:", el);
                    return el;
                }
            }
             if (el === document.body || el === document.documentElement) {
                break;
            }
        }

        const docEl = document.documentElement;
        const maxPgScr = docEl.scrollHeight - window.innerHeight;
        const curPgScr = _getScrTop(window);

        if ((e.deltaY < 0 && curPgScr > 0.1) || (e.deltaY > 0 && curPgScr < maxPgScr - 0.1)) {
             if (cfg.dbg) console.log("SS: Using win scroll");
            return window;
        }

        if (cfg.dbg) console.log("SS: No scroll target found.");
        return null;
    }

    function _getPxDel(e, tgtEl) {
        let delta = e.deltaY;
        if (e.deltaMode === 1) {
            delta *= cfg.lnHt;
        } else if (e.deltaMode === 2) {
            const clHt = (tgtEl === window) ? window.innerHeight : /** @type {Element} */ (tgtEl).clientHeight;
            delta *= clHt * 0.9;
        }
        return delta;
    }

    function _hdlWheel(e) {
        if (e.deltaX !== 0 || e.ctrlKey || e.altKey ) {
             if (cfg.dbg) console.log("SS: Ignore event (X/mod)", e);
            return;
        }

        const tgtEl = _getTgt(e);

        if (!tgtEl) {
             if (cfg.dbg) console.log("SS: No target, native scroll");
            return;
        }

        e.preventDefault();

        const pxDel = _getPxDel(e, tgtEl);
        _startOrUpd(tgtEl, pxDel);
    }

    function _hdlClick(e) {
        const path = e.composedPath ? e.composedPath() : [];
        for (const el of path) {
            if (el instanceof Element || el === window) {
                _cancelAnim(el);
            }
             if (el === window) break;
        }
         _cancelAnim(window);
    }

    function _init() {
        if (window.top !== window.self && !window.location.href.match(/debug=true/)) {
            console.log("SS: Iframe detected, skip.");
            return;
        }
        if (window.SSEnhLoaded_NC) { // Changed flag slightly
             console.log("SS: Already loaded.");
            return;
        }

        document.documentElement.addEventListener('wheel', _hdlWheel, { passive: false, capture: true });
        document.documentElement.addEventListener('mousedown', _hdlClick, { passive: true, capture: true });
        document.documentElement.addEventListener('touchstart', _hdlClick, { passive: true, capture: true });

        window.SSEnhLoaded_NC = true;
        console.log(`Enhanced Smooth Scroll (Short+FX, No Comments): Initialized (v3.3) | Motion Blur: ${cfg.mBlur}`);
        if (cfg.dbg) console.log("SS: Debug mode enabled.");
    }

    _init();

})();