Smooth Scroll

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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

})();