Smooth Scroll

Universal smooth scrolling for mouse wheel only. Touchpad uses native scrolling.

当前为 2024-11-22 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name Smooth Scroll
// @description Universal smooth scrolling for mouse wheel only. Touchpad uses native scrolling.
// @author DXRK1E
// @icon https://i.imgur.com/IAwk6NN.png
// @include *
// @exclude     https://www.youtube.com/*
// @exclude     https://mail.google.com/*
// @version 2.3
// @namespace sttb-dxrk1e
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    class SmoothScroll {
        constructor() {
            this.config = {
                smoothness: 0.8,        // Increased for more stability
                acceleration: 0.25,      // Reduced to prevent jumps
                minDelta: 0.5,          // Increased minimum threshold
                maxRefreshRate: 144,     // Reduced max refresh rate
                minRefreshRate: 30,
                defaultRefreshRate: 60,
                debug: false
            };

            this.state = {
                isLoaded: false,
                lastFrameTime: 0,
                lastWheelTime: 0,
                lastDelta: 0,
                scrollHistory: [],
                activeScrollElements: new WeakMap()
            };

            this.handleWheel = this.handleWheel.bind(this);
            this.handleClick = this.handleClick.bind(this);
            this.animateScroll = this.animateScroll.bind(this);
            this.detectScrollDevice = this.detectScrollDevice.bind(this);
        }

        init() {
            if (window.top !== window.self || this.state.isLoaded) {
                return;
            }

            if (!window.requestAnimationFrame) {
                window.requestAnimationFrame =
                    window.mozRequestAnimationFrame ||
                    window.webkitRequestAnimationFrame ||
                    window.msRequestAnimationFrame ||
                    ((cb) => setTimeout(cb, 1000 / 60));
            }

            document.addEventListener('wheel', this.handleWheel, {
                passive: false,
                capture: true
            });

            document.addEventListener('mousedown', this.handleClick, true);
            document.addEventListener('touchstart', this.handleClick, true);

            document.addEventListener('visibilitychange', () => {
                if (document.hidden) {
                    this.clearAllScrolls();
                }
            });

            this.state.isLoaded = true;
            this.log('Smooth Scroll Activated (Mouse Only)');
        }

        detectScrollDevice(event) {
            const now = performance.now();
            const timeDelta = now - this.state.lastWheelTime;

            // Update scroll history for better detection
            this.state.scrollHistory.push({
                delta: event.deltaY,
                time: now,
                mode: event.deltaMode
            });

            // Keep only last 5 events
            if (this.state.scrollHistory.length > 5) {
                this.state.scrollHistory.shift();
            }

            // Analyze scroll pattern
            const isConsistent = this.analyzeScrollPattern();

            // More accurate touchpad detection
            const isTouchpad = (
                (Math.abs(event.deltaY) < 5 && event.deltaMode === 0) ||  // Very small precise deltas
                (timeDelta < 32 && this.state.scrollHistory.length > 2) || // Rapid small movements
                !isConsistent || // Inconsistent scroll pattern
                (event.deltaMode === 0 && !Number.isInteger(event.deltaY)) // Fractional pixels
            );

            this.state.lastWheelTime = now;
            this.state.lastDelta = event.deltaY;

            return isTouchpad;
        }

        analyzeScrollPattern() {
            if (this.state.scrollHistory.length < 3) return true;

            const deltas = this.state.scrollHistory.map(entry => entry.delta);
            const avgDelta = deltas.reduce((a, b) => a + Math.abs(b), 0) / deltas.length;

            // Check if deltas are relatively consistent (characteristic of mouse wheels)
            return deltas.every(delta =>
                Math.abs(Math.abs(delta) - avgDelta) < avgDelta * 0.5
            );
        }

        log(...args) {
            if (this.config.debug) {
                console.log('[Smooth Scroll]', ...args);
            }
        }

        getCurrentRefreshRate(timestamp) {
            const frameTime = timestamp - (this.state.lastFrameTime || timestamp);
            this.state.lastFrameTime = timestamp;

            const fps = 1000 / Math.max(frameTime, 1);
            return Math.min(
                Math.max(fps, this.config.minRefreshRate),
                this.config.maxRefreshRate
            );
        }

        getScrollableParents(element, direction) {
            const scrollables = [];

            while (element && element !== document.body) {
                if (this.isScrollable(element, direction)) {
                    scrollables.push(element);
                }
                element = element.parentElement;
            }

            if (this.isScrollable(document.body, direction)) {
                scrollables.push(document.body);
            }

            return scrollables;
        }

        isScrollable(element, direction) {
            if (!element || element === window || element === document) {
                return false;
            }

            const style = window.getComputedStyle(element);
            const overflowY = style['overflow-y'];

            if (overflowY === 'hidden' || overflowY === 'visible') {
                return false;
            }

            const scrollTop = element.scrollTop;
            const scrollHeight = element.scrollHeight;
            const clientHeight = element.clientHeight;

            return direction < 0 ?
                scrollTop > 0 :
                Math.ceil(scrollTop + clientHeight) < scrollHeight;
        }

        handleWheel(event) {
            if (event.defaultPrevented || window.getSelection().toString()) {
                return;
            }

            // If using touchpad, let native scrolling handle it
            if (this.detectScrollDevice(event)) {
                return;
            }

            const scrollables = this.getScrollableParents(event.target, Math.sign(event.deltaY));
            if (!scrollables.length) {
                return;
            }

            const target = scrollables[0];
            let delta = event.deltaY;

            // Normalize delta based on mode
            if (event.deltaMode === 1) { // LINE mode
                const lineHeight = parseInt(getComputedStyle(target).lineHeight) || 20;
                delta *= lineHeight;
            } else if (event.deltaMode === 2) { // PAGE mode
                delta *= target.clientHeight;
            }

            // Apply a more consistent delta transformation
            delta = Math.sign(delta) * Math.sqrt(Math.abs(delta)) * 10;

            this.scroll(target, delta);
            event.preventDefault();
        }

        handleClick(event) {
            const elements = this.getScrollableParents(event.target, 0);
            elements.forEach(element => this.stopScroll(element));
        }

        scroll(element, delta) {
            if (!this.state.activeScrollElements.has(element)) {
                this.state.activeScrollElements.set(element, {
                    pixels: 0,
                    subpixels: 0,
                    direction: Math.sign(delta)
                });
            }

            const scrollData = this.state.activeScrollElements.get(element);

            // Only accumulate scroll if in same direction or very small remaining scroll
            if (Math.sign(delta) === scrollData.direction || Math.abs(scrollData.pixels) < 1) {
                const acceleration = Math.min(
                    1 + (Math.abs(scrollData.pixels) * this.config.acceleration),
                    2
                );
                scrollData.pixels += delta * acceleration;
                scrollData.direction = Math.sign(delta);
            } else {
                // If direction changed, reset acceleration
                scrollData.pixels = delta;
                scrollData.direction = Math.sign(delta);
            }

            if (!scrollData.animating) {
                scrollData.animating = true;
                this.animateScroll(element);
            }
        }

        stopScroll(element) {
            if (this.state.activeScrollElements.has(element)) {
                const scrollData = this.state.activeScrollElements.get(element);
                scrollData.pixels = 0;
                scrollData.subpixels = 0;
                scrollData.animating = false;
            }
        }

        clearAllScrolls() {
            this.state.activeScrollElements = new WeakMap();
        }

        animateScroll(element) {
            if (!this.state.activeScrollElements.has(element)) {
                return;
            }

            const scrollData = this.state.activeScrollElements.get(element);

            if (Math.abs(scrollData.pixels) < this.config.minDelta) {
                scrollData.animating = false;
                return;
            }

            requestAnimationFrame((timestamp) => {
                const refreshRate = this.getCurrentRefreshRate(timestamp);
                const smoothnessFactor = Math.pow(refreshRate, -1 / (refreshRate * this.config.smoothness));

                // More stable scroll amount calculation
                const scrollAmount = scrollData.pixels * (1 - smoothnessFactor);
                const integerPart = Math.trunc(scrollAmount);

                // Accumulate subpixels more accurately
                scrollData.subpixels += (scrollAmount - integerPart);
                let additionalPixels = Math.trunc(scrollData.subpixels);
                scrollData.subpixels -= additionalPixels;

                const totalScroll = integerPart + additionalPixels;

                // Only update if we have a meaningful scroll amount
                if (Math.abs(totalScroll) >= 1) {
                    scrollData.pixels -= totalScroll;

                    try {
                        element.scrollTop += totalScroll;
                    } catch (error) {
                        this.log('Scroll error:', error);
                        this.stopScroll(element);
                        return;
                    }
                }

                if (scrollData.animating) {
                    this.animateScroll(element);
                }
            });
        }
    }

    // Initialize
    const smoothScroll = new SmoothScroll();
    smoothScroll.init();
})();