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