Smooth Scroll

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

// ==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();

})();