Liquid glass effect scroll button
// ==UserScript==
// @name Scroll Button
// @name:zh 滚动按钮
// @namespace https://greasyfork.org/
// @version 2.4
// @description Liquid glass effect scroll button
// @description:zh 液态玻璃效果滚动按钮
// @match *://*/*
// @license MIT
// @grant none
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
if (window !== window.top) {
return;
}
function throttle(func, delay) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall < delay) {
return;
}
lastCall = now;
return func.apply(this, args);
};
}
function createButton(iconBase64, altText, buttonStyles) {
const btn = document.createElement('button');
btn.type = 'button';
const img = document.createElement('img');
img.src = `data:image/svg+xml;base64,${iconBase64}`;
img.alt = altText;
img.style.width = '16px';
img.style.height = '16px';
img.style.display = 'block';
btn.appendChild(img);
Object.assign(btn.style, buttonStyles);
return btn;
}
function setHoverShadow(button) {
button.addEventListener('mouseenter', function() {
button.style.boxShadow = '0 4px 8px rgba(0,0,0,0.25)';
});
button.addEventListener('mouseleave', function() {
button.style.boxShadow = '0 1px 3px rgba(0,0,0,0.15)';
});
}
function getScrollContainer() {
const docEl = document.scrollingElement || document.documentElement;
if (docEl.scrollHeight - docEl.clientHeight > 1) {
return docEl;
}
const candidates = document.querySelectorAll('body *');
let best = docEl;
let maxScrollable = 0;
candidates.forEach(el => {
const scrollable = el.scrollHeight - el.clientHeight;
if (scrollable <= 1) {
return;
}
const style = window.getComputedStyle(el);
if (!/(auto|scroll)/.test(style.overflowY)) {
return;
}
if (scrollable > maxScrollable) {
maxScrollable = scrollable;
best = el;
}
});
return best;
}
function getScrollableHeight(container) {
return Math.max(container.scrollHeight - container.clientHeight, 0);
}
function scrollContainerBy(container, amount) {
if (container === document.scrollingElement || container === document.documentElement) {
window.scrollBy({ top: amount, behavior: 'smooth' });
} else {
container.scrollBy({ top: amount, behavior: 'smooth' });
}
}
function getScrollTop(container) {
if (container === document.scrollingElement || container === document.documentElement) {
return window.scrollY;
}
return container.scrollTop;
}
function init() {
if (!document.body) {
// 如果 document.body 不存在,等待 DOMContentLoaded 事件
window.addEventListener('DOMContentLoaded', init);
return;
}
// Base64-encoded SVG data for bottom icon
const base64BottomIcon =
'PHN2ZyBzdHJva2U9ImN1cnJlbnRDb2xvciIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIyIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgY2xhc3M9Imljb24tc20gbS0xIiBoZWlnaHQ9IjFlbSIgd2lkdGg9IjFlbSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48bGluZSB4MT0iMTIiIHkxPSI1IiB4Mj0iMTIiIHkyPSIxOSI+PC9saW5lPjxwb2x5bGluZSBwb2ludHM9IjE5IDEyIDEyIDE5IDUgMTIiPjwvcG9seWxpbmU+PC9zdmc+';
// Base64-encoded SVG data for top icon
const base64TopIcon =
'PHN2ZyBzdHJva2U9ImN1cnJlbnRDb2xvciIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIyIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgY2xhc3M9Imljb24tc20gbS0xIiBoZWlnaHQ9IjFlbSIgd2lkdGg9IjFlbSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48bGluZSB4MT0iMTIiIHkxPSIxOSIgeDI9IjEyIiB5Mj0iNSI+PC9saW5lPjxwb2x5bGluZSBwb2ludHM9IjUgMTIgMTIgNSAxOSAxMiI+PC9wb2x5bGluZT48L3N2Zz4=';
// Common styles for both buttons
const buttonStyles = {
position: 'fixed',
zIndex: '99',
width: '25px',
height: '25px',
minWidth: '25px',
minHeight: '25px',
maxWidth: '25px',
maxHeight: '25px',
flexShrink: '0',
boxSizing: 'border-box',
backgroundColor: 'transparent',
backdropFilter: 'blur(8px)',
WebkitBackdropFilter: 'blur(8px)',
boxShadow: '0 1px 3px rgba(0,0,0,0.35)',
border: '0.5px solid transparent',
borderRadius: '50%',
padding: '4px',
margin: '0',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'opacity 0.2s, box-shadow 0.2s'
};
// 按钮显示/隐藏状态
let buttonsHidden = false;
function applyButtonsVisibility() {
if (buttonsHidden) {
bottomButton.style.setProperty('display', 'none', 'important');
topButton.style.setProperty('display', 'none', 'important');
} else {
bottomButton.style.setProperty('display', 'flex', 'important');
toggleTopButton();
}
}
function toggleButtonsVisibility() {
buttonsHidden = !buttonsHidden;
applyButtonsVisibility();
}
const bottomButton = createButton(base64BottomIcon, 'Scroll to Bottom', buttonStyles);
bottomButton.style.bottom = '14px';
bottomButton.style.right = '14px';
bottomButton.addEventListener('click', function() {
const container = getScrollContainer();
scrollContainerBy(container, getScrollableHeight(container) * 0.55);
});
const topButton = createButton(base64TopIcon, 'Scroll to Top', buttonStyles);
topButton.style.bottom = '50px';
topButton.style.right = '14px';
topButton.addEventListener('click', function() {
const container = getScrollContainer();
scrollContainerBy(container, -getScrollableHeight(container) * 0.75);
});
document.body.append(bottomButton, topButton);
setHoverShadow(bottomButton);
setHoverShadow(topButton);
// 智能避让:检测右下角是否有其他浮动元素,自动上移按钮
function avoidOverlap() {
const baseBottom = 16;
const gap = 36;
const buttonSize = 32;
const vw = window.innerWidth;
const vh = window.innerHeight;
let maxOffset = 0;
const nodes = document.querySelectorAll('body *');
for (let i = 0; i < nodes.length; i++) {
const el = nodes[i];
if (el === bottomButton || el === topButton) continue;
const style = window.getComputedStyle(el);
// 只关心真正浮动层
if (style.position !== 'fixed' && style.position !== 'sticky') continue;
if (style.display === 'none' || style.visibility === 'hidden') continue;
if (parseFloat(style.opacity) === 0) continue;
const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) continue;
const isOversized = rect.width > vw * 0.6 || rect.height > vh * 0.5;
if (isOversized) continue;
const hitRight = rect.right > vw - 80;
const hitBottom = rect.bottom > vh - 80;
if (hitRight && hitBottom) {
const overlapHeight = rect.bottom - (vh - baseBottom);
if (overlapHeight > maxOffset) {
maxOffset = overlapHeight;
}
}
}
let offset = Math.max(0, maxOffset + 8);
const maxAllowedOffset = Math.max(vh - baseBottom - gap - buttonSize - 8, 0);
offset = Math.min(offset, maxAllowedOffset);
bottomButton.style.bottom = `${baseBottom + offset}px`;
topButton.style.bottom = `${baseBottom + gap + offset}px`;
}
avoidOverlap();
window.addEventListener('scroll', throttle(avoidOverlap, 300), { capture: true });
window.addEventListener('resize', throttle(avoidOverlap, 300));
const observer = new MutationObserver(throttle(avoidOverlap, 500));
observer.observe(document.body, { childList: true, subtree: true });
function toggleTopButton() {
if (buttonsHidden) {
topButton.style.display = 'none';
return;
}
const container = getScrollContainer();
topButton.style.display = getScrollTop(container) === 0 ? 'none' : 'flex';
}
toggleTopButton();
window.addEventListener('scroll', throttle(toggleTopButton, 100), { capture: true });
// 快捷键 Ctrl+Z:切换按钮显示/隐藏
window.addEventListener('keydown', function(e) {
if (e.ctrlKey && !e.metaKey && !e.altKey && (e.key === 'z' || e.key === 'Z')) {
e.preventDefault();
toggleButtonsVisibility();
}
});
const activeTouchDownTimes = new Map();
const SIMULTANEOUS_THRESHOLD = 200; // 毫秒
window.addEventListener('pointerdown', function(e) {
if (e.pointerType !== 'touch') {
return;
}
const now = Date.now();
activeTouchDownTimes.set(e.pointerId, now);
if (activeTouchDownTimes.size === 2) {
const times = Array.from(activeTouchDownTimes.values());
const gap = Math.abs(times[0] - times[1]);
if (gap <= SIMULTANEOUS_THRESHOLD) {
toggleButtonsVisibility();
}
}
}, { passive: true, capture: true });
function releaseTouchPointer(e) {
if (e.pointerType === 'touch') {
activeTouchDownTimes.delete(e.pointerId);
}
}
window.addEventListener('pointerup', releaseTouchPointer, { passive: true, capture: true });
window.addEventListener('pointercancel', releaseTouchPointer, { passive: true, capture: true });
}
setTimeout(init, 1000);
})();