// ==UserScript== // @name 极速滚动页面神器 // @namespace https://github.com/xkzm123 // @version 1.0 // @description 自动滚动页面,支持亚像素级平滑滚动,非线性速度控制,悬浮窗全状态可拖动。 // @author xkzm // @match *://*/* // @license MIT // @grant none // @run-at document-end // @downloadURL none // ==/UserScript== (function () { 'use strict'; // 防止重复加载 if (window.suyinScrollLoaded) return; window.suyinScrollLoaded = true; // --- 核心状态 --- let state = { isScrolling: false, speed: 50, sliderValue: 22, lastTime: 0, pixelAccumulator: 0, isMinimized: false, requestId: null, drag: { isDragging: false, startX: 0, startY: 0, initialLeft: 0, initialTop: 0, hasMoved: false } }; // --- 样式定义 --- const css = ` :host { all: initial; /* 重置继承的样式 */ font-family: system-ui, -apple-system, sans-serif; z-index: 2147483647; position: fixed; top: 100px; right: 20px; } #panel-container { width: 180px; background: rgba(20, 20, 20, 0.9); backdrop-filter: blur(8px); color: #fff; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.4); border: 1px solid rgba(255,255,255,0.15); transition: width 0.2s, height 0.2s, opacity 0.2s; user-select: none; overflow: hidden; } /* 最小化状态 */ #panel-container.minimized { width: 48px; height: 48px; border-radius: 50%; cursor: move; background: rgba(76, 175, 80, 0.9); } #panel-container.minimized:active { transform: scale(0.95); } #panel-container.minimized .panel-content, #panel-container.minimized .panel-header { display: none; } #panel-container.minimized .minimized-icon { display: flex; } .panel-header { padding: 12px 15px; background: rgba(255,255,255,0.08); cursor: move; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid rgba(255,255,255,0.1); } .panel-title { font-size: 13px; font-weight: 600; color: #ddd; } .minimize-btn { cursor: pointer; width: 22px; height: 22px; line-height: 20px; text-align: center; border-radius: 4px; background: rgba(255,255,255,0.1); font-size: 16px; transition: 0.2s; } .minimize-btn:hover { background: #ff9800; color: #000; } .panel-content { padding: 15px; display: flex; flex-direction: column; gap: 12px; } .speed-control { display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: #aaa; } .speed-val { font-family: 'Menlo', monospace; color: #4CAF50; font-weight: bold; font-size: 14px; } input[type=range] { width: 100%; height: 5px; background: rgba(255,255,255,0.2); border-radius: 3px; appearance: none; outline: none; cursor: pointer; } input[type=range]::-webkit-slider-thumb { appearance: none; width: 16px; height: 16px; border-radius: 50%; background: #4CAF50; box-shadow: 0 0 5px rgba(0,0,0,0.5); transition: transform 0.1s; } input[type=range]::-webkit-slider-thumb:hover { transform: scale(1.2); } button { width: 100%; padding: 10px 0; border: none; border-radius: 8px; background: #4CAF50; color: white; font-weight: bold; font-size: 14px; cursor: pointer; transition: background 0.2s; } button:hover { filter: brightness(1.1); } button.scrolling { background: #f44336; } .minimized-icon { display: none; width: 100%; height: 100%; align-items: center; justify-content: center; font-size: 24px; color: #fff; } .hint { font-size:10px; color:#666; text-align:center; margin-top:0px; } `; // --- 逻辑函数 --- function calculateSpeed(val) { const maxSpeed = 500; const percentage = val / 100; let rawSpeed = maxSpeed * Math.pow(percentage, 2.5); if (val > 0 && rawSpeed < 1) rawSpeed = 1; if (val === 0) rawSpeed = 0; return Math.floor(rawSpeed); } function animationLoop(timestamp) { if (!state.isScrolling) return; if (!state.lastTime) state.lastTime = timestamp; const deltaTime = timestamp - state.lastTime; state.lastTime = timestamp; state.pixelAccumulator += (state.speed * deltaTime) / 1000; const pixelsToScroll = Math.trunc(state.pixelAccumulator); if (pixelsToScroll !== 0) { window.scrollBy(0, pixelsToScroll); state.pixelAccumulator -= pixelsToScroll; } if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight - 1) { toggleScrolling(false); return; } state.requestId = requestAnimationFrame(animationLoop); } function toggleScrolling(forceState, shadowRoot) { if (typeof forceState !== 'undefined') state.isScrolling = forceState; else state.isScrolling = !state.isScrolling; // 需要穿透 Shadow DOM 获取按钮 const btn = shadowRoot.getElementById('toggle-scroll-btn'); if (btn) { if (state.isScrolling) { btn.textContent = '停止滚动'; btn.classList.add('scrolling'); state.lastTime = 0; state.pixelAccumulator = 0; state.requestId = requestAnimationFrame(animationLoop); } else { btn.textContent = '开始滚动'; btn.classList.remove('scrolling'); if (state.requestId) cancelAnimationFrame(state.requestId); state.requestId = null; } } } // --- UI 构建 (Shadow DOM) --- function createUI() { const host = document.createElement('div'); host.id = 'suyin-scroll-host'; document.body.appendChild(host); const shadow = host.attachShadow({ mode: 'open' }); // 注入样式 const styleTag = document.createElement('style'); styleTag.textContent = css; shadow.appendChild(styleTag); // 注入HTML const container = document.createElement('div'); container.id = 'panel-container'; state.speed = calculateSpeed(state.sliderValue); container.innerHTML = `
平滑滚动
速度 ${state.speed} px/s
Alt+Z 开始 / Alt+X 停止
`; shadow.appendChild(container); // --- 事件绑定 (在 Shadow DOM 内部查找元素) --- const slider = shadow.getElementById('speed-slider'); const speedDisplay = container.querySelector('.speed-val'); const btn = shadow.getElementById('toggle-scroll-btn'); const minBtn = container.querySelector('.minimize-btn'); slider.addEventListener('input', (e) => { state.sliderValue = parseInt(e.target.value); state.speed = calculateSpeed(state.sliderValue); speedDisplay.textContent = `${state.speed} px/s`; }); btn.addEventListener('click', () => toggleScrolling(undefined, shadow)); minBtn.addEventListener('click', (e) => { e.stopPropagation(); state.isMinimized = true; container.classList.add('minimized'); }); // 拖拽逻辑需要调整作用域 initDrag(host, container); // 拖动的是 host,控制的是 container 样式或 host 位置 // 全局键盘事件 document.addEventListener('keydown', (e) => { if (['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName)) return; if (e.altKey && e.code === 'KeyZ') toggleScrolling(true, shadow); else if (e.altKey && e.code === 'KeyX') toggleScrolling(false, shadow); }); } // --- 拖拽逻辑 (适配 Shadow DOM) --- function initDrag(host, container) { // 拖动事件监听在 shadow root 的 container 上 container.addEventListener('mousedown', (e) => { const isHeader = e.target.closest('.panel-header'); const isControl = e.target.closest('input') || e.target.closest('button'); if (!state.isMinimized && !isHeader) return; if (isControl) return; state.drag.isDragging = true; state.drag.hasMoved = false; state.drag.startX = e.clientX; state.drag.startY = e.clientY; // 获取 host 的位置(因为 host 是 fixed) const rect = host.getBoundingClientRect(); state.drag.initialLeft = rect.left; state.drag.initialTop = rect.top; // 设置 host 的位置 host.style.right = 'auto'; host.style.left = `${rect.left}px`; host.style.top = `${rect.top}px`; container.style.transition = 'none'; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!state.drag.isDragging) return; const dx = e.clientX - state.drag.startX; const dy = e.clientY - state.drag.startY; if (Math.abs(dx) > 2 || Math.abs(dy) > 2) state.drag.hasMoved = true; host.style.left = `${state.drag.initialLeft + dx}px`; host.style.top = `${state.drag.initialTop + dy}px`; }); document.addEventListener('mouseup', () => { if (state.drag.isDragging) { state.drag.isDragging = false; container.style.transition = 'width 0.2s, height 0.2s, opacity 0.2s'; } }); container.addEventListener('click', () => { if (state.isMinimized && !state.drag.hasMoved) { state.isMinimized = false; container.classList.remove('minimized'); } }); } if (document.readyState === 'loading') { window.addEventListener('DOMContentLoaded', createUI); } else { createUI(); } })();