// ==UserScript==
// @name Smooth Scroll
// @description Configurable smooth scroll with optional motion blur. Uses requestAnimationFrame (like V-Sync).
// @author DARK1E
// @icon https://i.imgur.com/IAwk6NN.png
// @include *
// @version 3.3
// @namespace sttb-dxrk1e
// @license MIT
// @grant none
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
const cfg = {
smth: 0.85,
stpMult: 1,
accDelFct: 0.2,
accMaxMult: 3,
thrsh: 1,
lnHt: 20,
mBlur: false,
mBlurInt: 0.3,
dbg: false,
};
const stMap = new WeakMap();
const DAMP_FCT = 1 - cfg.smth;
const ACC_TMO = 150;
const MAX_BLUR = 5;
const BLUR_THRESH = 0.2;
function _animStep(el) {
const st = stMap.get(el);
if (!st) return;
const curScrTop = _getScrTop(el);
const delta = st.tgtY - st.curY;
if (Math.abs(delta) < cfg.thrsh && Math.abs(curScrTop - st.tgtY) < cfg.thrsh) {
if (cfg.dbg) console.log("SS: Anim end", el);
if (Math.abs(curScrTop - st.tgtY) > 0.1) {
_setScrTop(el, Math.round(st.tgtY));
}
_cancelAnim(el);
return;
}
const step = delta * DAMP_FCT;
st.curY += step;
const scrAmt = Math.round(st.curY) - curScrTop;
if (scrAmt !== 0) {
const origBehav = _setBehav(el, 'auto');
_setScrTop(el, curScrTop + scrAmt);
}
if (cfg.mBlur) {
const blurPx = Math.min(MAX_BLUR, Math.abs(step) * cfg.mBlurInt);
if (blurPx > BLUR_THRESH) {
_setFilter(el, `blur(${blurPx.toFixed(1)}px)`);
} else {
_setFilter(el, 'none');
}
}
st.animId = requestAnimationFrame(() => _animStep(el));
}
function _startOrUpd(el, dY) {
let st = stMap.get(el);
const now = performance.now();
if (!st) {
st = {
tgtY: _getScrTop(el),
curY: _getScrTop(el),
animId: null,
ts: 0,
mult: 1,
};
stMap.set(el, st);
}
const dt = now - st.ts;
if (dt < ACC_TMO) {
const accInc = Math.abs(dY) * cfg.accDelFct / cfg.lnHt;
st.mult = Math.min(cfg.accMaxMult, st.mult + accInc);
} else {
st.mult = 1;
}
st.ts = now;
const effDel = dY * st.mult * cfg.stpMult;
st.tgtY += effDel;
st.tgtY = _clampTgt(el, st.tgtY);
if (cfg.dbg) {
console.log(`SS: Upd Tgt`, el, `| dY: ${dY.toFixed(2)}`, `| mult: ${st.mult.toFixed(2)}`, `| effDel: ${effDel.toFixed(2)}`, `| tgtY: ${st.tgtY.toFixed(2)}`);
}
if (!st.animId) {
st.curY = _getScrTop(el);
if (cfg.dbg) console.log("SS: Start anim", el);
st.animId = requestAnimationFrame(() => _animStep(el));
}
}
function _cancelAnim(el) {
const st = stMap.get(el);
if (st?.animId) {
cancelAnimationFrame(st.animId);
stMap.delete(el);
if (cfg.dbg) console.log("SS: Anim cancelled", el);
}
if (cfg.mBlur) {
_setFilter(el, 'none');
}
}
function _getScrTop(el) {
return (el === window) ? (window.scrollY || document.documentElement.scrollTop) : /** @type {Element} */ (el).scrollTop;
}
function _setScrTop(el, val) {
if (el === window) {
document.documentElement.scrollTop = val;
} else {
/** @type {Element} */ (el).scrollTop = val;
}
}
function _setBehav(el, behav) {
const target = (el === window) ? document.documentElement : el;
if (target instanceof Element) {
const orig = target.style.scrollBehavior;
target.style.scrollBehavior = behav;
return orig;
}
return undefined;
}
function _setFilter(el, val) {
const target = (el === window) ? document.documentElement : el;
if (target instanceof HTMLElement) {
try {
target.style.filter = val;
} catch (e) {
if (cfg.dbg) console.warn("SS: Failed to set filter on", target, e);
}
}
}
function _clampTgt(el, tgtY) {
let maxScr;
if (el === window) {
maxScr = document.documentElement.scrollHeight - window.innerHeight;
} else {
const htmlEl = /** @type {Element} */ (el);
maxScr = htmlEl.scrollHeight - htmlEl.clientHeight;
}
return Math.max(0, Math.min(tgtY, maxScr));
}
function _isScr(el) {
if (!el || !(el instanceof Element) || el === document.documentElement || el === document.body) {
return false;
}
try {
const style = window.getComputedStyle(el);
const ovf = style.overflowY;
const isOvf = ovf === 'scroll' || ovf === 'auto';
const canScr = el.scrollHeight > el.clientHeight + 1;
return isOvf && canScr;
} catch (e) {
if (cfg.dbg) console.warn("SS: Err check scroll", el, e);
return false;
}
}
function _getTgt(e) {
const path = e.composedPath ? e.composedPath() : [];
for (const el of path) {
if (!(el instanceof Element)) continue;
if (_isScr(el)) {
const curScr = _getScrTop(el);
const maxScr = el.scrollHeight - el.clientHeight;
if ((e.deltaY < 0 && curScr > 0.1) || (e.deltaY > 0 && curScr < maxScr - 0.1)) {
if (cfg.dbg) console.log("SS: Found el in path:", el);
return el;
}
}
if (el === document.body || el === document.documentElement) {
break;
}
}
const docEl = document.documentElement;
const maxPgScr = docEl.scrollHeight - window.innerHeight;
const curPgScr = _getScrTop(window);
if ((e.deltaY < 0 && curPgScr > 0.1) || (e.deltaY > 0 && curPgScr < maxPgScr - 0.1)) {
if (cfg.dbg) console.log("SS: Using win scroll");
return window;
}
if (cfg.dbg) console.log("SS: No scroll target found.");
return null;
}
function _getPxDel(e, tgtEl) {
let delta = e.deltaY;
if (e.deltaMode === 1) {
delta *= cfg.lnHt;
} else if (e.deltaMode === 2) {
const clHt = (tgtEl === window) ? window.innerHeight : /** @type {Element} */ (tgtEl).clientHeight;
delta *= clHt * 0.9;
}
return delta;
}
function _hdlWheel(e) {
if (e.deltaX !== 0 || e.ctrlKey || e.altKey ) {
if (cfg.dbg) console.log("SS: Ignore event (X/mod)", e);
return;
}
const tgtEl = _getTgt(e);
if (!tgtEl) {
if (cfg.dbg) console.log("SS: No target, native scroll");
return;
}
e.preventDefault();
const pxDel = _getPxDel(e, tgtEl);
_startOrUpd(tgtEl, pxDel);
}
function _hdlClick(e) {
const path = e.composedPath ? e.composedPath() : [];
for (const el of path) {
if (el instanceof Element || el === window) {
_cancelAnim(el);
}
if (el === window) break;
}
_cancelAnim(window);
}
function _init() {
if (window.top !== window.self && !window.location.href.match(/debug=true/)) {
console.log("SS: Iframe detected, skip.");
return;
}
if (window.SSEnhLoaded_NC) { // Changed flag slightly
console.log("SS: Already loaded.");
return;
}
document.documentElement.addEventListener('wheel', _hdlWheel, { passive: false, capture: true });
document.documentElement.addEventListener('mousedown', _hdlClick, { passive: true, capture: true });
document.documentElement.addEventListener('touchstart', _hdlClick, { passive: true, capture: true });
window.SSEnhLoaded_NC = true;
console.log(`Enhanced Smooth Scroll (Short+FX, No Comments): Initialized (v3.3) | Motion Blur: ${cfg.mBlur}`);
if (cfg.dbg) console.log("SS: Debug mode enabled.");
}
_init();
})();