Scroll to Top Button

Adds a customizable scroll-to-top button near the page bottom.

目前為 2025-01-29 提交的版本,檢視 最新版本

// ==UserScript==
// @name        Scroll to Top Button
// @namespace   sttb-ujs-dxrk1e
// @description Adds a customizable scroll-to-top button near the page bottom.
// @icon https://i.imgur.com/FxF8TLS.png
// @match       *://*/*
// @grant       none
// @version     2.4.0
// @author      DXRK1E
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    const APP_CONFIG = {
        button: {
            size: '45px',
            fontSize: '18px',
            backgroundColor: '#3a3a3a',
            hoverColor: '#555',
            textColor: '#f5f5f5',
            borderRadius: '55%',
            position: {
                bottom: '25px',
                right: '25px'
            },
            shadow: '0 3px 10px rgba(0,0,0,0.45)',
            transitionSpeed: 350,
            zIndex: 2147483647,
            svg: {
               width: '20px',
               height: '20px',
               viewBox: '0 0 16 16'
            }
        },
        behavior: {
            showThreshold: 350,
            bottomThreshold: 200, // Pixels from the bottom
            debounceDelay: 175,
            enableSmoothScroll: true,
        },
        scroll: {
              duration: 900,  // Duration of scroll animation in ms
              easing: 'easeInOutCubic', // Easing function for smooth acceleration/deceleration
            fps: 60,  // Frames per second for smooth animation
            breakpoints: {  // Adjust scroll speed based on distance
                short: 550,    // px
                medium: 1600,  // px
                long: 3200     // px
            }
        }
    };


    const EASING_FUNCTIONS = {
          easeInOutCubic: t => t < 0.5
            ? 4 * t * t * t
            : 1 - Math.pow(-2 * t + 2, 3) / 2,

        easeOutQuad: t => 1 - (1 - t) * (1 - t),

        easeInOutExpo: t => t === 0
            ? 0
            : t === 1
            ? 1
            : t < 0.5
            ? Math.pow(2, 20 * t - 10) / 2
            : (2 - Math.pow(2, -20 * t + 10)) / 2,
    };

    function createButtonElement() {
        const button = document.createElement('button');
        button.id = 'enhanced-scroll-button';
        button.innerHTML = `
            <svg width="${APP_CONFIG.button.svg.width}" height="${APP_CONFIG.button.svg.height}" viewBox="${APP_CONFIG.button.svg.viewBox}" fill="none" xmlns="http://www.w3.org/2000/svg">
                <path d="M8 3L14 9L12.6 10.4L8 5.8L3.4 10.4L2 9L8 3Z" fill="currentColor"/>
            </svg>
        `;

        Object.assign(button.style, {
             position: 'fixed',
            bottom: APP_CONFIG.button.position.bottom,
            right: APP_CONFIG.button.position.right,
            width: APP_CONFIG.button.size,
            height: APP_CONFIG.button.size,
            fontSize: APP_CONFIG.button.fontSize,
            backgroundColor: APP_CONFIG.button.backgroundColor,
            color: APP_CONFIG.button.textColor,
            border: 'none',
            borderRadius: APP_CONFIG.button.borderRadius,
            cursor: 'pointer',
            boxShadow: APP_CONFIG.button.shadow,
            opacity: '0',
            visibility: 'hidden',
             zIndex: APP_CONFIG.button.zIndex,
            transition: `all ${APP_CONFIG.button.transitionSpeed}ms ease-in-out`,
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            padding: '0',
            transform: 'scale(1)',
             outline: 'none'
         });

        button.addEventListener('mouseenter', () => setButtonHoverStyle(button));
        button.addEventListener('mouseleave', () => resetButtonStyles(button));
        button.addEventListener('mousedown', () => setButtonPressStyle(button));
        button.addEventListener('mouseup', () => setButtonHoverStyle(button));

        return button;
    }

     function setButtonHoverStyle(button) {
        button.style.backgroundColor = APP_CONFIG.button.hoverColor;
        button.style.transform = 'scale(1.1)';
    }

    function resetButtonStyles(button) {
        button.style.backgroundColor = APP_CONFIG.button.backgroundColor;
        button.style.transform = 'scale(1)';
    }

    function setButtonPressStyle(button) {
         button.style.transform = 'scale(0.95)';
     }


    function smoothScrollToTop() {
        if(!APP_CONFIG.behavior.enableSmoothScroll) {
           window.scrollTo({ top: 0, behavior: 'auto' });
            return;
        }

        const startPosition = getScrollPosition();
        const startTime = performance.now();
         let scrollDuration = APP_CONFIG.scroll.duration;


        if (startPosition < APP_CONFIG.scroll.breakpoints.short) {
              scrollDuration *= 0.7;
         } else if (startPosition > APP_CONFIG.scroll.breakpoints.long) {
              scrollDuration *= 1.3;
         }

         function animationStep(currentTime) {
              const timeElapsed = currentTime - startTime;
              const progress = Math.min(timeElapsed / scrollDuration, 1);
              const easedProgress = EASING_FUNCTIONS[APP_CONFIG.scroll.easing](progress);
              const newPosition = startPosition - (startPosition * easedProgress);

            window.scrollTo(0, newPosition);

             if (timeElapsed < scrollDuration) {
               requestAnimationFrame(animationStep);
           }
         }
        requestAnimationFrame(animationStep);
    }

   function getScrollPosition() {
        return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
    }


    function debounce(func, wait, immediate = false) {
        let timeoutId;
        let lastArgs;

      return function(...args) {
           lastArgs = args;

          const later = () => {
             timeoutId = null;
              if (!immediate) {
                  func.apply(this, lastArgs);
              }
            };

          const callNow = immediate && !timeoutId;
        clearTimeout(timeoutId);
          timeoutId = setTimeout(later, wait);
            if (callNow) {
             func.apply(this, lastArgs);
        }
      };
    }


    function shouldShowButton() {
          const scrollHeight = Math.max(
              document.documentElement.scrollHeight,
              document.body.scrollHeight
          );
          const viewportHeight = window.innerHeight;
          const scrollTop = getScrollPosition();
          const bottomScroll = scrollHeight - viewportHeight - APP_CONFIG.behavior.bottomThreshold;

           return (
               scrollTop > APP_CONFIG.behavior.showThreshold &&
               scrollTop >= bottomScroll
           );
    }


    function handleScrollEvent() {
         const button = document.getElementById('enhanced-scroll-button');
         if (!button) return;

        if (shouldShowButton()) {
            button.style.visibility = 'visible';
           button.style.opacity = '1';
         } else {
             button.style.opacity = '0';
              setTimeout(() => {
                 if (button.style.opacity === '0') {
                     button.style.visibility = 'hidden';
                 }
             }, APP_CONFIG.button.transitionSpeed);
         }
     }

    function initialize() {
      const existingButton = document.getElementById('enhanced-scroll-button');
         if (existingButton) return;

        const scrollButton = createButtonElement();
      document.body.appendChild(scrollButton);

     const debouncedScrollHandler = debounce(handleScrollEvent, APP_CONFIG.behavior.debounceDelay);

      window.addEventListener('scroll', debouncedScrollHandler, { passive: true });
        window.addEventListener('resize', debouncedScrollHandler, { passive: true });


        const mutationObserver = new MutationObserver(debouncedScrollHandler);
        mutationObserver.observe(document.documentElement, {
           childList: true,
           subtree: true
        });

         scrollButton.addEventListener('click', (event) => {
             event.preventDefault();
             smoothScrollToTop();
        });

        handleScrollEvent(); // Initial button state
    }

    // Initialization on DOM ready
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', initialize);
    } else {
        initialize();
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址