墨水屏电纸书划动优化

消除划动动画 可设置划动倍率和更新间隔 支持手势缩放和双击复位

目前為 2025-10-06 提交的版本,檢視 最新版本

// ==UserScript==
// @name         墨水屏电纸书划动优化
// @namespace    cc.cxuan.books
// @version      1.28
// @description  消除划动动画  可设置划动倍率和更新间隔  支持手势缩放和双击复位
// @match        *://*/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @run-at       document-end
// @noframes
// @license      MIT
// @author       cxuan.cc
// ==/UserScript==
(function(){
    // 设置项
    let mx = GM_getValue('multiplierX', 5),
        my = GM_getValue('multiplierY', 2),
        intervalSec = GM_getValue('interval', 0),
        enableZoom = GM_getValue('enableZoom', true);
    GM_registerMenuCommand(`设置X轴移动倍率(当前 ${mx})`, ()=>{
        let v = parseFloat(prompt('X 轴滑动倍率:', mx));
        if(!isNaN(v)){ GM_setValue('multiplierX', mx=v); location.reload(); }
    });
    GM_registerMenuCommand(`设置Y轴移动倍率(当前 ${my})`, ()=>{
        let v = parseFloat(prompt('Y 轴滑动倍率:', my));
        if(!isNaN(v)){ GM_setValue('multiplierY', my=v); location.reload(); }
    });
    GM_registerMenuCommand(`设置更新间隔(当前 ${intervalSec}s)`, ()=>{
        let v = parseFloat(prompt('更新间隔(秒,0=仅松手时刷新):', intervalSec));
        if(!isNaN(v)){ GM_setValue('interval', intervalSec=v); location.reload(); }
    });
    GM_registerMenuCommand(`切换双指放大/双击复位(当前 ${enableZoom?'开':'关'})`, ()=>{
        GM_setValue('enableZoom', enableZoom=!enableZoom);
        location.reload();
    });

    // 准备动态 meta viewport
    let meta = document.querySelector('meta[name=viewport]');
    if(!meta){
        meta = document.createElement('meta');
        meta.name = 'viewport';
        document.head.appendChild(meta);
    }
    const originalMeta = meta.content;
    const m1 = /initial-scale=([0-9.]+)/.exec(originalMeta);
    const defaultScale = m1 ? parseFloat(m1[1]) : 1;
    let currentScale = defaultScale, pinch = null;
    function updateMeta(s){
        meta.content = `width=device-width, initial-scale=${s}, maximum-scale=10, user-scalable=yes`;
    }

    // 滑动逻辑
    const touchMap = {}, 
          periodicUpdate = ()=>{
        Object.values(touchMap).forEach(info=>{
            if(info.cx==null||info.cy==null) return;
            let tdX=(info.sx-info.cx)*mx,
                tdY=(info.sy-info.cy)*my,
                dX=tdX-(info.lastDx||0),
                dY=tdY-(info.lastDy||0);
            if(dX) info.el.scrollLeft+=dX;
            if(dY) info.el.scrollTop+=dY;
            info.lastDx=tdX; info.lastDy=tdY;
        });
    };
    let timerId = null;
    function startTimer(){
        if(intervalSec>0 && !timerId)
            timerId = setInterval(periodicUpdate, intervalSec*1000);
    }
    function stopTimer(){
        if(timerId){ clearInterval(timerId); timerId = null; }
    }

    document.addEventListener('touchstart', e=>{
        // 双指缩放
        if(enableZoom && e.touches.length===2){
            let [t1,t2] = e.touches,
                d = Math.hypot(t2.clientX-t1.clientX, t2.clientY-t1.clientY);
            pinch = { initialDist: d, initialScale: currentScale };
            e.preventDefault(); return;
        }
        if(e.touches.length>1) return;
        for(let t of e.changedTouches){
            let el = t.target;
            while(el && el!==document){
                let s = getComputedStyle(el),
                    canY = el.scrollHeight>el.clientHeight && /auto|scroll/.test(s.overflowY),
                    canX = el.scrollWidth>el.clientWidth && /auto|scroll/.test(s.overflowX);
                if(canY||canX) break;
                el = el.parentNode;
            }
            if(!el || el===document) el = document.scrollingElement||document.documentElement;
            touchMap[t.identifier] = {
                sx:t.clientX, sy:t.clientY,
                cx:t.clientX, cy:t.clientY,
                el, lastDx:0, lastDy:0
            };
        }
        startTimer();
    }, { passive:false });

    document.addEventListener('touchmove', e=>{
        if(pinch){
            let [t1,t2] = e.touches,
                d = Math.hypot(t2.clientX-t1.clientX, t2.clientY-t1.clientY),
                sc = pinch.initialScale * (d / pinch.initialDist);
            currentScale = parseFloat(sc.toFixed(2));
            updateMeta(currentScale);
            e.preventDefault(); return;
        }
        if(e.touches.length>1) return;
        let doPrevent = false;
        for(let t of e.changedTouches){
            let info = touchMap[t.identifier];
            if(!info) continue;
            info.cx = t.clientX; info.cy = t.clientY;
            if(!(info.el === (document.scrollingElement||document.documentElement)
               && info.el.scrollTop===0
               && t.clientY-info.sy>0)) doPrevent = true;
        }
        if(doPrevent) e.preventDefault();
    }, { passive:false });

    function finishTouch(e){
        if(pinch && e.touches.length<2){
            pinch = null; return;
        }
        if(e.touches.length>1) return;
        for(let t of e.changedTouches){
            let info = touchMap[t.identifier];
            if(!info) continue;
            if(intervalSec===0){
                let dx=(info.sx-t.clientX)*mx,
                    dy=(info.sy-t.clientY)*my;
                info.el.scrollLeft+=dx;
                info.el.scrollTop+=dy;
            } else {
                info.cx = t.clientX;
                info.cy = t.clientY;
                periodicUpdate();
            }
            delete touchMap[t.identifier];
        }
        if(!Object.keys(touchMap).length) stopTimer();
    }
    document.addEventListener('touchend', finishTouch, { passive:false });
    document.addEventListener('touchcancel', finishTouch, { passive:false });

    // 双击复位
    let lastTap = 0;
    document.addEventListener('touchend', e=>{
        if(!enableZoom) return;
        if(e.touches.length===0 && e.changedTouches.length===1){
            let now = Date.now();
            if(now - lastTap < 300){
                let de = document.scrollingElement||document.documentElement,
                    lx = de.scrollLeft, ly = de.scrollTop;
                currentScale = defaultScale;
                updateMeta(currentScale);
                de.scrollLeft = lx; de.scrollTop = ly;
            }
            lastTap = now;
        }
    }, { passive:false });
})();