Greasy Fork is available in English.

Scroll to Top Button

Adds a scroll to top button at the bottom right of the page when scrolled to the bottom, and hides it at the top.

// ==UserScript==
// @name        Scroll to Top Button
// @namespace   sttb-ujs-dxrk1e
// @description  Adds a scroll to top button at the bottom right of the page when scrolled to the bottom, and hides it at the top.
// @icon https://i.imgur.com/FxF8TLS.png
// @match       *://*/*
// @grant       none
// @version     2.1.1
// @author      DXRK1E
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // Configuration object
    const CONFIG = {
        buttonSize: '40px',
        fontSize: '16px',
        backgroundColor: '#333',
        hoverColor: '#444',
        textColor: '#FFF',
        borderRadius: '50%',
        bottom: '20px',
        right: '20px',
        showThreshold: 300,
        minimumPageHeight: 1000,
        fadeSpeed: 300,
        debounceDelay: 150,
        zIndex: 2147483647,
        // Smooth scroll configuration
        scroll: {
            duration: 800,  // 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: 500,    // px
                medium: 1500,  // px
                long: 3000     // px
            }
        }
    };

    // Easing functions
    const easingFunctions = {
        // Cubic easing in/out
        easeInOutCubic: t => t < 0.5 
            ? 4 * t * t * t 
            : 1 - Math.pow(-2 * t + 2, 3) / 2,
        
        // Quadratic easing out
        easeOutQuad: t => 1 - (1 - t) * (1 - t),
        
        // Exponential easing in/out
        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 createScrollButton() {
        const button = document.createElement('button');
        button.id = 'enhanced-scroll-top-btn';
        button.innerHTML = `
            <svg width="16" height="16" viewBox="0 0 16 16" 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>
        `;

        const styles = {
            position: 'fixed',
            bottom: CONFIG.bottom,
            right: CONFIG.right,
            width: CONFIG.buttonSize,
            height: CONFIG.buttonSize,
            fontSize: CONFIG.fontSize,
            backgroundColor: CONFIG.backgroundColor,
            color: CONFIG.textColor,
            border: 'none',
            borderRadius: CONFIG.borderRadius,
            cursor: 'pointer',
            boxShadow: '0 2px 8px rgba(0,0,0,0.4)',
            opacity: '0',
            visibility: 'hidden',
            zIndex: CONFIG.zIndex,
            transition: `all ${CONFIG.fadeSpeed}ms ease-in-out`,
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            padding: '0',
            transform: 'scale(1)',
            outline: 'none'
        };

        Object.assign(button.style, styles);

        button.addEventListener('mouseenter', () => {
            button.style.backgroundColor = CONFIG.hoverColor;
            button.style.transform = 'scale(1.1)';
        });

        button.addEventListener('mouseleave', () => {
            button.style.backgroundColor = CONFIG.backgroundColor;
            button.style.transform = 'scale(1)';
        });

        button.addEventListener('mousedown', () => {
            button.style.transform = 'scale(0.95)';
        });

        button.addEventListener('mouseup', () => {
            button.style.transform = 'scale(1.1)';
        });

        return button;
    }

    function smoothScrollToTop() {
        const startPosition = window.pageYOffset || document.documentElement.scrollTop;
        const startTime = performance.now();
        let duration = CONFIG.scroll.duration;

        // Adjust duration based on scroll distance
        if (startPosition < CONFIG.scroll.breakpoints.short) {
            duration *= 0.7;  // Faster for short distances
        } else if (startPosition > CONFIG.scroll.breakpoints.long) {
            duration *= 1.3;  // Slower for long distances
        }

        function scrollStep(currentTime) {
            const timeElapsed = currentTime - startTime;
            const progress = Math.min(timeElapsed / duration, 1);

            const easedProgress = easingFunctions[CONFIG.scroll.easing](progress);
            
            const position = startPosition - (startPosition * easedProgress);
            
            window.scrollTo(0, position);

            if (timeElapsed < duration) {
                requestAnimationFrame(scrollStep);
            }
        }

        requestAnimationFrame(scrollStep);
    }

    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    function shouldShowButton() {
        const scrollHeight = Math.max(
            document.documentElement.scrollHeight,
            document.body.scrollHeight
        );
        const viewportHeight = window.innerHeight;
        const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
        
        return (
            scrollTop > CONFIG.showThreshold &&
            scrollHeight > CONFIG.minimumPageHeight &&
            scrollHeight > viewportHeight * 1.5
        );
    }

    function handleScroll() {
        const button = document.getElementById('enhanced-scroll-top-btn');
        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';
                }
            }, CONFIG.fadeSpeed);
        }
    }

    function init() {
        const existingButton = document.getElementById('enhanced-scroll-top-btn');
        if (existingButton) return;

        const button = createScrollButton();
        document.body.appendChild(button);

        const debouncedScroll = debounce(handleScroll, CONFIG.debounceDelay);
        window.addEventListener('scroll', debouncedScroll, { passive: true });
        window.addEventListener('resize', debouncedScroll, { passive: true });
        
        const observer = new MutationObserver(debouncedScroll);
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        button.addEventListener('click', (e) => {
            e.preventDefault();
            smoothScrollToTop();
        });

        // Initial check
        handleScroll();
    }

    // Initialize when DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();