您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Enables smooth page scrolling using JavaScript. Improved from an initial concept by Winceptor.
当前为
// ==UserScript== // @name Smooth Scroll // @description Enables smooth page scrolling using JavaScript. Improved from an initial concept by Winceptor. // @author DXRK1E // @icon https://i.imgur.com/IAwk6NN.png // @include * // @version 2.4 // @namespace sttb-dxrk1e // @license MIT // @grant none // ==/UserScript== (function () { 'use strict'; const SmoothScroll = {}; // Settings (Adjust these to your preference) SmoothScroll.settings = { scrollSmoothness: 0.5, // Controls how smooth the animation is (0-1, lower is smoother) scrollAcceleration: 0.5, // Controls how quickly the scroll speed increases debugMode: 0, // Debugging (Set to 1 for console logs, 0 for none) targetRefreshRate: 60, // Animation refresh rate target maxRefreshRate: 300, // Animation refresh rate upper limit minRefreshRate: 1, // Animation refresh rate lower limit animationDuration: 500, // Max duration of smooth animation in milliseconds scrollThreshold: 2, // Minimum distance to trigger smooth scrolling passiveEventListeners: false, // Set to true if you want to use passive event listeners }; // Animation state tracking const scrollState = new WeakMap(); // Use WeakMap to avoid memory leaks // --- Helper Functions for Scroll Data Management --- // Manages sub-pixel scroll offset for smoother animation function getSubPixelOffset(element, newOffset) { let elementState = scrollState.get(element) || {}; if (newOffset !== undefined) { elementState.subPixelOffset = newOffset; } scrollState.set(element, elementState); return elementState.subPixelOffset || 0; } // Manages accumulated scroll amount in integer pixels function getPixelScrollAmount(element, newAmount) { let elementState = scrollState.get(element) || {}; if (newAmount !== undefined) { elementState.pixelScrollAmount = newAmount; getSubPixelOffset(element, 0); // Reset subpixel offset when full pixels are scrolled } scrollState.set(element, elementState); return elementState.pixelScrollAmount || 0; } // --- Core Animation Logic --- function animateScrolling(targetElement, refreshRate, startTime = performance.now()) { let currentSubPixelOffset = getSubPixelOffset(targetElement); let currentPixelScroll = getPixelScrollAmount(targetElement); const currentTime = performance.now(); const elapsedTime = currentTime - startTime; const scrollDirection = currentPixelScroll > 0 ? 1 : currentPixelScroll < 0 ? -1 : 0; const scrollRatio = 1 - Math.pow(refreshRate, -1 / (refreshRate * SmoothScroll.settings.scrollSmoothness)); const scrollChange = currentPixelScroll * scrollRatio; if ((Math.abs(currentPixelScroll) > SmoothScroll.settings.scrollThreshold) && elapsedTime < SmoothScroll.settings.animationDuration ) { const fullPixelScrolls = Math.floor(Math.abs(scrollChange)) * scrollDirection; const subPixelChange = scrollChange - fullPixelScrolls; const additionalPixelScrolls = Math.floor(Math.abs(currentSubPixelOffset + subPixelChange)) * scrollDirection; const remainingSubPixelOffset = currentSubPixelOffset + subPixelChange - additionalPixelScrolls; getPixelScrollAmount(targetElement, currentPixelScroll - fullPixelScrolls - additionalPixelScrolls); getSubPixelOffset(targetElement, remainingSubPixelOffset); const totalScrollDelta = fullPixelScrolls + additionalPixelScrolls; targetElement.style.scrollBehavior = "auto"; // Ensure smooth scroll doesn't interfere targetElement.scrollTop += totalScrollDelta; targetElement.isScrolling = true; requestAnimationFrameUpdate(newRefreshRate => { animateScrolling(targetElement, newRefreshRate, startTime); }); } else { requestAnimationFrameUpdate(() => { getPixelScrollAmount(targetElement, 0); // Reset scroll amount once complete }); targetElement.isScrolling = false; } } // Used to get a dynamically calculated refresh rate function requestAnimationFrameUpdate(callback) { const startTime = performance.now(); window.requestAnimationFrame(() => { const endTime = performance.now(); const frameDuration = endTime - startTime; const calculatedFps = 1000 / Math.max(frameDuration, 1); const refreshRate = Math.min(Math.max(calculatedFps, SmoothScroll.settings.minRefreshRate), SmoothScroll.settings.maxRefreshRate); callback(refreshRate); }); } // --- Exposed API --- SmoothScroll.stop = function (targetElement) { if (targetElement) { getPixelScrollAmount(targetElement, 0); } }; SmoothScroll.start = function (targetElement, scrollAmount) { if (targetElement) { const currentScrollAmount = getPixelScrollAmount(targetElement, scrollAmount); if (!targetElement.isScrolling) { animateScrolling(targetElement, SmoothScroll.settings.targetRefreshRate); } } }; // --- Helper functions for detecting scrollable elements --- // Checks if an element is scrollable vertically function canScroll(element, direction) { if (direction < 0) { return element.scrollTop > 0; // Check if can scroll up } if (direction > 0) { // check if can scroll down if (element === document.body) { // Special body case if(element.scrollTop === 0) { element.scrollTop = 3; if(element.scrollTop === 0){ return false; } element.scrollTop = 0; } return Math.round(element.clientHeight + element.scrollTop) < element.offsetHeight; } return Math.round(element.clientHeight + element.scrollTop) < element.scrollHeight; } return false; // No direction, so can't scroll } // Checks if an element has a scrollbar (and isn't set to not scroll) function hasScrollbar(element) { if (element === window || element === document) { return false; // Window and Document always have scroll (if needed) } if(element === document.body) { return window.getComputedStyle(document.body)['overflow-y'] !== "hidden"; } if (element === document.documentElement) { return window.innerWidth > document.documentElement.clientWidth; } const style = window.getComputedStyle(element); return style['overflow-y'] !== "hidden" && style['overflow-y'] !== "visible"; } // Checks both scrollability and scrollbar presence function isScrollable(element, direction) { if(element === document.body) { // return false; } const canScrollCheck = canScroll(element, direction); if (!canScrollCheck) { return false; } const hasScrollbarCheck = hasScrollbar(element); if (!hasScrollbarCheck) { return false; } return true; } // ---- Event Handling Utilities --- function getEventPath(event) { if (event.path) { return event.path; } if (event.composedPath) { return event.composedPath(); } return null; } // Finds the first scrollable parent element in the event path function getScrollTarget(event) { const direction = event.deltaY; const path = getEventPath(event); if (!path) { return null; } for (let i = 0; i < path.length; i++) { const element = path[i]; if (isScrollable(element, direction)) { return element; } } return null; } // Get style property with error-checking function getStyleProperty(element, styleProperty) { try { if (window.getComputedStyle) { const value = document.defaultView.getComputedStyle(element, null).getPropertyValue(styleProperty); if (value) { return parseInt(value, 10); } } else if (element.currentStyle) { const value = element.currentStyle[styleProperty.encamel()]; if (value) { return parseInt(value, 10); } } } catch (e) { if(SmoothScroll.settings.debugMode) { console.error(`Error getting style property ${styleProperty} on element:`, element, e); } return null; } return null; } // --- Event Handlers --- function stopActiveScroll(event) { const path = getEventPath(event); if (!path) { return; } path.forEach(element => SmoothScroll.stop(element)); } function startSmoothScroll(event, targetElement) { if (event.defaultPrevented) { return; // Ignore if prevented already } let deltaAmount = event.deltaY; // Adjust delta based on deltaMode (lines/pages) if (event.deltaMode && event.deltaMode === 1) { const lineHeight = getStyleProperty(targetElement, 'line-height'); if (lineHeight && lineHeight > 0) { deltaAmount *= lineHeight; } } if (event.deltaMode && event.deltaMode === 2) { const pageHeight = targetElement.clientHeight; if (pageHeight && pageHeight > 0) { deltaAmount *= pageHeight; } } const currentPixelAmount = getPixelScrollAmount(targetElement); const accelerationRatio = Math.sqrt(Math.abs(currentPixelAmount / deltaAmount * SmoothScroll.settings.scrollAcceleration)); const acceleration = Math.round(deltaAmount * accelerationRatio); const totalScroll = currentPixelAmount + deltaAmount + acceleration; SmoothScroll.start(targetElement, totalScroll); event.preventDefault(); // Prevents default behavior } function handleWheel(event) { const targetElement = getScrollTarget(event); if (targetElement) { startSmoothScroll(event, targetElement); } } // Handles click event to stop scrolling animation if it is in progress. function handleClick(event) { stopActiveScroll(event); } // --- Initialization --- function initialize() { if (window.top !== window.self) { return; // Exit if in an iframe } if (window.SmoothScroll && window.SmoothScroll.isLoaded) { return; // Exit if already loaded } if (!window.requestAnimationFrame) { // Fallback for older browsers window.requestAnimationFrame = window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame; } const eventOptions = SmoothScroll.settings.passiveEventListeners ? { passive: false } : false; document.documentElement.addEventListener("wheel", function (e) { handleWheel(e); }, eventOptions); // disable passive listener to prevent scroll. document.documentElement.addEventListener("mousedown", function (e) { handleClick(e); }); window.SmoothScroll = SmoothScroll; window.SmoothScroll.isLoaded = true; if (SmoothScroll.settings.debugMode > 0) { console.log("Enhanced Smooth Scroll: loaded"); } } // Polyfill for String.encamel (if not supported) if (!String.prototype.encamel) { String.prototype.encamel = function() { return this.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); }); }; } initialize(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址