// ==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 = `