Smooth Scroll

Universal smooth scrolling for mouse wheel only. Touchpad uses native scrolling.

当前为 2024-11-22 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Smooth Scroll
  3. // @description Universal smooth scrolling for mouse wheel only. Touchpad uses native scrolling.
  4. // @author DXRK1E
  5. // @icon https://i.imgur.com/IAwk6NN.png
  6. // @include *
  7. // @exclude https://www.youtube.com/*
  8. // @exclude https://mail.google.com/*
  9. // @version 2.3
  10. // @namespace sttb-dxrk1e
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. class SmoothScroll {
  18. constructor() {
  19. this.config = {
  20. smoothness: 0.8, // Increased for more stability
  21. acceleration: 0.25, // Reduced to prevent jumps
  22. minDelta: 0.5, // Increased minimum threshold
  23. maxRefreshRate: 144, // Reduced max refresh rate
  24. minRefreshRate: 30,
  25. defaultRefreshRate: 60,
  26. debug: false
  27. };
  28.  
  29. this.state = {
  30. isLoaded: false,
  31. lastFrameTime: 0,
  32. lastWheelTime: 0,
  33. lastDelta: 0,
  34. scrollHistory: [],
  35. activeScrollElements: new WeakMap()
  36. };
  37.  
  38. this.handleWheel = this.handleWheel.bind(this);
  39. this.handleClick = this.handleClick.bind(this);
  40. this.animateScroll = this.animateScroll.bind(this);
  41. this.detectScrollDevice = this.detectScrollDevice.bind(this);
  42. }
  43.  
  44. init() {
  45. if (window.top !== window.self || this.state.isLoaded) {
  46. return;
  47. }
  48.  
  49. if (!window.requestAnimationFrame) {
  50. window.requestAnimationFrame =
  51. window.mozRequestAnimationFrame ||
  52. window.webkitRequestAnimationFrame ||
  53. window.msRequestAnimationFrame ||
  54. ((cb) => setTimeout(cb, 1000 / 60));
  55. }
  56.  
  57. document.addEventListener('wheel', this.handleWheel, {
  58. passive: false,
  59. capture: true
  60. });
  61.  
  62. document.addEventListener('mousedown', this.handleClick, true);
  63. document.addEventListener('touchstart', this.handleClick, true);
  64.  
  65. document.addEventListener('visibilitychange', () => {
  66. if (document.hidden) {
  67. this.clearAllScrolls();
  68. }
  69. });
  70.  
  71. this.state.isLoaded = true;
  72. this.log('Smooth Scroll Activated (Mouse Only)');
  73. }
  74.  
  75. detectScrollDevice(event) {
  76. const now = performance.now();
  77. const timeDelta = now - this.state.lastWheelTime;
  78.  
  79. // Update scroll history for better detection
  80. this.state.scrollHistory.push({
  81. delta: event.deltaY,
  82. time: now,
  83. mode: event.deltaMode
  84. });
  85.  
  86. // Keep only last 5 events
  87. if (this.state.scrollHistory.length > 5) {
  88. this.state.scrollHistory.shift();
  89. }
  90.  
  91. // Analyze scroll pattern
  92. const isConsistent = this.analyzeScrollPattern();
  93.  
  94. // More accurate touchpad detection
  95. const isTouchpad = (
  96. (Math.abs(event.deltaY) < 5 && event.deltaMode === 0) || // Very small precise deltas
  97. (timeDelta < 32 && this.state.scrollHistory.length > 2) || // Rapid small movements
  98. !isConsistent || // Inconsistent scroll pattern
  99. (event.deltaMode === 0 && !Number.isInteger(event.deltaY)) // Fractional pixels
  100. );
  101.  
  102. this.state.lastWheelTime = now;
  103. this.state.lastDelta = event.deltaY;
  104.  
  105. return isTouchpad;
  106. }
  107.  
  108. analyzeScrollPattern() {
  109. if (this.state.scrollHistory.length < 3) return true;
  110.  
  111. const deltas = this.state.scrollHistory.map(entry => entry.delta);
  112. const avgDelta = deltas.reduce((a, b) => a + Math.abs(b), 0) / deltas.length;
  113.  
  114. // Check if deltas are relatively consistent (characteristic of mouse wheels)
  115. return deltas.every(delta =>
  116. Math.abs(Math.abs(delta) - avgDelta) < avgDelta * 0.5
  117. );
  118. }
  119.  
  120. log(...args) {
  121. if (this.config.debug) {
  122. console.log('[Smooth Scroll]', ...args);
  123. }
  124. }
  125.  
  126. getCurrentRefreshRate(timestamp) {
  127. const frameTime = timestamp - (this.state.lastFrameTime || timestamp);
  128. this.state.lastFrameTime = timestamp;
  129.  
  130. const fps = 1000 / Math.max(frameTime, 1);
  131. return Math.min(
  132. Math.max(fps, this.config.minRefreshRate),
  133. this.config.maxRefreshRate
  134. );
  135. }
  136.  
  137. getScrollableParents(element, direction) {
  138. const scrollables = [];
  139.  
  140. while (element && element !== document.body) {
  141. if (this.isScrollable(element, direction)) {
  142. scrollables.push(element);
  143. }
  144. element = element.parentElement;
  145. }
  146.  
  147. if (this.isScrollable(document.body, direction)) {
  148. scrollables.push(document.body);
  149. }
  150.  
  151. return scrollables;
  152. }
  153.  
  154. isScrollable(element, direction) {
  155. if (!element || element === window || element === document) {
  156. return false;
  157. }
  158.  
  159. const style = window.getComputedStyle(element);
  160. const overflowY = style['overflow-y'];
  161.  
  162. if (overflowY === 'hidden' || overflowY === 'visible') {
  163. return false;
  164. }
  165.  
  166. const scrollTop = element.scrollTop;
  167. const scrollHeight = element.scrollHeight;
  168. const clientHeight = element.clientHeight;
  169.  
  170. return direction < 0 ?
  171. scrollTop > 0 :
  172. Math.ceil(scrollTop + clientHeight) < scrollHeight;
  173. }
  174.  
  175. handleWheel(event) {
  176. if (event.defaultPrevented || window.getSelection().toString()) {
  177. return;
  178. }
  179.  
  180. // If using touchpad, let native scrolling handle it
  181. if (this.detectScrollDevice(event)) {
  182. return;
  183. }
  184.  
  185. const scrollables = this.getScrollableParents(event.target, Math.sign(event.deltaY));
  186. if (!scrollables.length) {
  187. return;
  188. }
  189.  
  190. const target = scrollables[0];
  191. let delta = event.deltaY;
  192.  
  193. // Normalize delta based on mode
  194. if (event.deltaMode === 1) { // LINE mode
  195. const lineHeight = parseInt(getComputedStyle(target).lineHeight) || 20;
  196. delta *= lineHeight;
  197. } else if (event.deltaMode === 2) { // PAGE mode
  198. delta *= target.clientHeight;
  199. }
  200.  
  201. // Apply a more consistent delta transformation
  202. delta = Math.sign(delta) * Math.sqrt(Math.abs(delta)) * 10;
  203.  
  204. this.scroll(target, delta);
  205. event.preventDefault();
  206. }
  207.  
  208. handleClick(event) {
  209. const elements = this.getScrollableParents(event.target, 0);
  210. elements.forEach(element => this.stopScroll(element));
  211. }
  212.  
  213. scroll(element, delta) {
  214. if (!this.state.activeScrollElements.has(element)) {
  215. this.state.activeScrollElements.set(element, {
  216. pixels: 0,
  217. subpixels: 0,
  218. direction: Math.sign(delta)
  219. });
  220. }
  221.  
  222. const scrollData = this.state.activeScrollElements.get(element);
  223.  
  224. // Only accumulate scroll if in same direction or very small remaining scroll
  225. if (Math.sign(delta) === scrollData.direction || Math.abs(scrollData.pixels) < 1) {
  226. const acceleration = Math.min(
  227. 1 + (Math.abs(scrollData.pixels) * this.config.acceleration),
  228. 2
  229. );
  230. scrollData.pixels += delta * acceleration;
  231. scrollData.direction = Math.sign(delta);
  232. } else {
  233. // If direction changed, reset acceleration
  234. scrollData.pixels = delta;
  235. scrollData.direction = Math.sign(delta);
  236. }
  237.  
  238. if (!scrollData.animating) {
  239. scrollData.animating = true;
  240. this.animateScroll(element);
  241. }
  242. }
  243.  
  244. stopScroll(element) {
  245. if (this.state.activeScrollElements.has(element)) {
  246. const scrollData = this.state.activeScrollElements.get(element);
  247. scrollData.pixels = 0;
  248. scrollData.subpixels = 0;
  249. scrollData.animating = false;
  250. }
  251. }
  252.  
  253. clearAllScrolls() {
  254. this.state.activeScrollElements = new WeakMap();
  255. }
  256.  
  257. animateScroll(element) {
  258. if (!this.state.activeScrollElements.has(element)) {
  259. return;
  260. }
  261.  
  262. const scrollData = this.state.activeScrollElements.get(element);
  263.  
  264. if (Math.abs(scrollData.pixels) < this.config.minDelta) {
  265. scrollData.animating = false;
  266. return;
  267. }
  268.  
  269. requestAnimationFrame((timestamp) => {
  270. const refreshRate = this.getCurrentRefreshRate(timestamp);
  271. const smoothnessFactor = Math.pow(refreshRate, -1 / (refreshRate * this.config.smoothness));
  272.  
  273. // More stable scroll amount calculation
  274. const scrollAmount = scrollData.pixels * (1 - smoothnessFactor);
  275. const integerPart = Math.trunc(scrollAmount);
  276.  
  277. // Accumulate subpixels more accurately
  278. scrollData.subpixels += (scrollAmount - integerPart);
  279. let additionalPixels = Math.trunc(scrollData.subpixels);
  280. scrollData.subpixels -= additionalPixels;
  281.  
  282. const totalScroll = integerPart + additionalPixels;
  283.  
  284. // Only update if we have a meaningful scroll amount
  285. if (Math.abs(totalScroll) >= 1) {
  286. scrollData.pixels -= totalScroll;
  287.  
  288. try {
  289. element.scrollTop += totalScroll;
  290. } catch (error) {
  291. this.log('Scroll error:', error);
  292. this.stopScroll(element);
  293. return;
  294. }
  295. }
  296.  
  297. if (scrollData.animating) {
  298. this.animateScroll(element);
  299. }
  300. });
  301. }
  302. }
  303.  
  304. // Initialize
  305. const smoothScroll = new SmoothScroll();
  306. smoothScroll.init();
  307. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址