Smooth Scroll

Configurable smooth scroll with optional motion blur. Uses requestAnimationFrame (like V-Sync).

  1. // ==UserScript==
  2. // @name Smooth Scroll
  3. // @description Configurable smooth scroll with optional motion blur. Uses requestAnimationFrame (like V-Sync).
  4. // @author DARK1E
  5. // @icon https://i.imgur.com/IAwk6NN.png
  6. // @include *
  7. // @version 3.3
  8. // @namespace sttb-dxrk1e
  9. // @license MIT
  10. // @grant none
  11. // @run-at document-start
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16.  
  17. const cfg = {
  18. smth: 0.85,
  19. stpMult: 1,
  20. accDelFct: 0.2,
  21. accMaxMult: 3,
  22. thrsh: 1,
  23. lnHt: 20,
  24. mBlur: false,
  25. mBlurInt: 0.3,
  26. dbg: false,
  27. };
  28.  
  29. const stMap = new WeakMap();
  30. const DAMP_FCT = 1 - cfg.smth;
  31. const ACC_TMO = 150;
  32. const MAX_BLUR = 5;
  33. const BLUR_THRESH = 0.2;
  34.  
  35. function _animStep(el) {
  36. const st = stMap.get(el);
  37. if (!st) return;
  38.  
  39. const curScrTop = _getScrTop(el);
  40. const delta = st.tgtY - st.curY;
  41.  
  42. if (Math.abs(delta) < cfg.thrsh && Math.abs(curScrTop - st.tgtY) < cfg.thrsh) {
  43. if (cfg.dbg) console.log("SS: Anim end", el);
  44. if (Math.abs(curScrTop - st.tgtY) > 0.1) {
  45. _setScrTop(el, Math.round(st.tgtY));
  46. }
  47. _cancelAnim(el);
  48. return;
  49. }
  50.  
  51. const step = delta * DAMP_FCT;
  52. st.curY += step;
  53.  
  54. const scrAmt = Math.round(st.curY) - curScrTop;
  55.  
  56. if (scrAmt !== 0) {
  57. const origBehav = _setBehav(el, 'auto');
  58. _setScrTop(el, curScrTop + scrAmt);
  59. }
  60.  
  61. if (cfg.mBlur) {
  62. const blurPx = Math.min(MAX_BLUR, Math.abs(step) * cfg.mBlurInt);
  63. if (blurPx > BLUR_THRESH) {
  64. _setFilter(el, `blur(${blurPx.toFixed(1)}px)`);
  65. } else {
  66. _setFilter(el, 'none');
  67. }
  68. }
  69.  
  70. st.animId = requestAnimationFrame(() => _animStep(el));
  71. }
  72.  
  73. function _startOrUpd(el, dY) {
  74. let st = stMap.get(el);
  75. const now = performance.now();
  76.  
  77. if (!st) {
  78. st = {
  79. tgtY: _getScrTop(el),
  80. curY: _getScrTop(el),
  81. animId: null,
  82. ts: 0,
  83. mult: 1,
  84. };
  85. stMap.set(el, st);
  86. }
  87.  
  88. const dt = now - st.ts;
  89. if (dt < ACC_TMO) {
  90. const accInc = Math.abs(dY) * cfg.accDelFct / cfg.lnHt;
  91. st.mult = Math.min(cfg.accMaxMult, st.mult + accInc);
  92. } else {
  93. st.mult = 1;
  94. }
  95. st.ts = now;
  96.  
  97. const effDel = dY * st.mult * cfg.stpMult;
  98. st.tgtY += effDel;
  99. st.tgtY = _clampTgt(el, st.tgtY);
  100.  
  101. if (cfg.dbg) {
  102. console.log(`SS: Upd Tgt`, el, `| dY: ${dY.toFixed(2)}`, `| mult: ${st.mult.toFixed(2)}`, `| effDel: ${effDel.toFixed(2)}`, `| tgtY: ${st.tgtY.toFixed(2)}`);
  103. }
  104.  
  105. if (!st.animId) {
  106. st.curY = _getScrTop(el);
  107. if (cfg.dbg) console.log("SS: Start anim", el);
  108. st.animId = requestAnimationFrame(() => _animStep(el));
  109. }
  110. }
  111.  
  112. function _cancelAnim(el) {
  113. const st = stMap.get(el);
  114. if (st?.animId) {
  115. cancelAnimationFrame(st.animId);
  116. stMap.delete(el);
  117. if (cfg.dbg) console.log("SS: Anim cancelled", el);
  118. }
  119. if (cfg.mBlur) {
  120. _setFilter(el, 'none');
  121. }
  122. }
  123.  
  124. function _getScrTop(el) {
  125. return (el === window) ? (window.scrollY || document.documentElement.scrollTop) : /** @type {Element} */ (el).scrollTop;
  126. }
  127.  
  128. function _setScrTop(el, val) {
  129. if (el === window) {
  130. document.documentElement.scrollTop = val;
  131. } else {
  132. /** @type {Element} */ (el).scrollTop = val;
  133. }
  134. }
  135.  
  136. function _setBehav(el, behav) {
  137. const target = (el === window) ? document.documentElement : el;
  138. if (target instanceof Element) {
  139. const orig = target.style.scrollBehavior;
  140. target.style.scrollBehavior = behav;
  141. return orig;
  142. }
  143. return undefined;
  144. }
  145.  
  146. function _setFilter(el, val) {
  147. const target = (el === window) ? document.documentElement : el;
  148. if (target instanceof HTMLElement) {
  149. try {
  150. target.style.filter = val;
  151. } catch (e) {
  152. if (cfg.dbg) console.warn("SS: Failed to set filter on", target, e);
  153. }
  154. }
  155. }
  156.  
  157. function _clampTgt(el, tgtY) {
  158. let maxScr;
  159. if (el === window) {
  160. maxScr = document.documentElement.scrollHeight - window.innerHeight;
  161. } else {
  162. const htmlEl = /** @type {Element} */ (el);
  163. maxScr = htmlEl.scrollHeight - htmlEl.clientHeight;
  164. }
  165. return Math.max(0, Math.min(tgtY, maxScr));
  166. }
  167.  
  168. function _isScr(el) {
  169. if (!el || !(el instanceof Element) || el === document.documentElement || el === document.body) {
  170. return false;
  171. }
  172. try {
  173. const style = window.getComputedStyle(el);
  174. const ovf = style.overflowY;
  175. const isOvf = ovf === 'scroll' || ovf === 'auto';
  176. const canScr = el.scrollHeight > el.clientHeight + 1;
  177. return isOvf && canScr;
  178. } catch (e) {
  179. if (cfg.dbg) console.warn("SS: Err check scroll", el, e);
  180. return false;
  181. }
  182. }
  183.  
  184. function _getTgt(e) {
  185. const path = e.composedPath ? e.composedPath() : [];
  186.  
  187. for (const el of path) {
  188. if (!(el instanceof Element)) continue;
  189.  
  190. if (_isScr(el)) {
  191. const curScr = _getScrTop(el);
  192. const maxScr = el.scrollHeight - el.clientHeight;
  193. if ((e.deltaY < 0 && curScr > 0.1) || (e.deltaY > 0 && curScr < maxScr - 0.1)) {
  194. if (cfg.dbg) console.log("SS: Found el in path:", el);
  195. return el;
  196. }
  197. }
  198. if (el === document.body || el === document.documentElement) {
  199. break;
  200. }
  201. }
  202.  
  203. const docEl = document.documentElement;
  204. const maxPgScr = docEl.scrollHeight - window.innerHeight;
  205. const curPgScr = _getScrTop(window);
  206.  
  207. if ((e.deltaY < 0 && curPgScr > 0.1) || (e.deltaY > 0 && curPgScr < maxPgScr - 0.1)) {
  208. if (cfg.dbg) console.log("SS: Using win scroll");
  209. return window;
  210. }
  211.  
  212. if (cfg.dbg) console.log("SS: No scroll target found.");
  213. return null;
  214. }
  215.  
  216. function _getPxDel(e, tgtEl) {
  217. let delta = e.deltaY;
  218. if (e.deltaMode === 1) {
  219. delta *= cfg.lnHt;
  220. } else if (e.deltaMode === 2) {
  221. const clHt = (tgtEl === window) ? window.innerHeight : /** @type {Element} */ (tgtEl).clientHeight;
  222. delta *= clHt * 0.9;
  223. }
  224. return delta;
  225. }
  226.  
  227. function _hdlWheel(e) {
  228. if (e.deltaX !== 0 || e.ctrlKey || e.altKey ) {
  229. if (cfg.dbg) console.log("SS: Ignore event (X/mod)", e);
  230. return;
  231. }
  232.  
  233. const tgtEl = _getTgt(e);
  234.  
  235. if (!tgtEl) {
  236. if (cfg.dbg) console.log("SS: No target, native scroll");
  237. return;
  238. }
  239.  
  240. e.preventDefault();
  241.  
  242. const pxDel = _getPxDel(e, tgtEl);
  243. _startOrUpd(tgtEl, pxDel);
  244. }
  245.  
  246. function _hdlClick(e) {
  247. const path = e.composedPath ? e.composedPath() : [];
  248. for (const el of path) {
  249. if (el instanceof Element || el === window) {
  250. _cancelAnim(el);
  251. }
  252. if (el === window) break;
  253. }
  254. _cancelAnim(window);
  255. }
  256.  
  257. function _init() {
  258. if (window.top !== window.self && !window.location.href.match(/debug=true/)) {
  259. console.log("SS: Iframe detected, skip.");
  260. return;
  261. }
  262. if (window.SSEnhLoaded_NC) { // Changed flag slightly
  263. console.log("SS: Already loaded.");
  264. return;
  265. }
  266.  
  267. document.documentElement.addEventListener('wheel', _hdlWheel, { passive: false, capture: true });
  268. document.documentElement.addEventListener('mousedown', _hdlClick, { passive: true, capture: true });
  269. document.documentElement.addEventListener('touchstart', _hdlClick, { passive: true, capture: true });
  270.  
  271. window.SSEnhLoaded_NC = true;
  272. console.log(`Enhanced Smooth Scroll (Short+FX, No Comments): Initialized (v3.3) | Motion Blur: ${cfg.mBlur}`);
  273. if (cfg.dbg) console.log("SS: Debug mode enabled.");
  274. }
  275.  
  276. _init();
  277.  
  278. })();