Web Speed Controller

control the speed of website timers, animations, videos, and Date.now

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Web Speed Controller
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  control the speed of website timers, animations, videos, and Date.now
// @author       Minoa
// @match        *://*/*
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // ─── UI ────────────────────────────────────────────────────────────────────

    const controls = document.createElement('div');
    controls.style.cssText = `
        position: fixed;
        top: 13px;
        right: 18px;
        background: rgba(15, 23, 42, 0.25);
        padding: 4px;
        border: 1px solid rgba(255, 255, 255, 0.08);
        border-radius: 8px;
        z-index: 9999999;
        display: flex;
        gap: 4px;
        box-shadow: 0 4px 16px rgba(0, 0, 0, 0.22);
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
        align-items: center;
        transition: all 0.3s ease;
        width: 45px;
        overflow: hidden;
    `;

    const input = document.createElement('input');
    input.type = 'number';
    input.step = '1';
    input.value = '1';
    input.style.cssText = `
        width: 22px;
        height: 22px;
        background: rgba(30, 41, 59, 0.3);
        border: 1px solid rgba(148, 163, 184, 0.1);
        color: rgba(226, 232, 240, 0.6);
        border-radius: 6px;
        padding: 2px;
        font-size: 12px;
        font-weight: 500;
        text-align: center;
        outline: none;
        transition: all 0.3s ease;
        -moz-appearance: textfield;
        cursor: pointer;
    `;

    const toggleButton = document.createElement('button');
    toggleButton.textContent = 'Enable';
    toggleButton.style.cssText = `
        background: #3b82f6;
        color: #ffffff;
        border: none;
        border-radius: 8px;
        width: 90px;
        height: 36px;
        font-size: 14px;
        font-weight: 600;
        cursor: pointer;
        transition: all 0.3s ease;
        display: none;
        align-items: center;
        justify-content: center;
        padding: 8px 16px;
        white-space: nowrap;
    `;

    let isExpanded = false;
    let isEnabled = false;

    controls.addEventListener('mouseenter', () => {
        if (!isExpanded) {
            controls.style.background = 'rgba(15, 23, 42, 0.45)';
            input.style.color = 'rgba(226, 232, 240, 0.8)';
        }
    });
    controls.addEventListener('mouseleave', () => {
        if (!isExpanded) {
            controls.style.background = 'rgba(15, 23, 42, 0.25)';
            input.style.color = 'rgba(226, 232, 240, 0.6)';
        }
    });

    function expandControls() {
        if (isExpanded) return;
        controls.style.cssText += `
            width: auto;
            padding: 16px;
            background: rgba(15, 23, 42, 0.85);
            backdrop-filter: blur(10px);
            -webkit-backdrop-filter: blur(10px);
            border-radius: 12px;
            gap: 12px;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
            border: 1px solid rgba(255, 255, 255, 0.1);
        `;
        input.style.cssText += `
            width: 70px;
            height: 36px;
            padding: 4px 8px;
            font-size: 14px;
            background: rgba(30, 41, 59, 0.8);
            border-radius: 8px;
            border: 2px solid rgba(148, 163, 184, 0.2);
            color: #e2e8f0;
        `;
        toggleButton.style.display = 'flex';
        isExpanded = true;
    }

    input.addEventListener('focus', () => {
        expandControls();
        input.style.borderColor = '#3b82f6';
        input.style.boxShadow = '0 0 0 3px rgba(59, 130, 246, 0.3)';
    });
    input.addEventListener('blur', () => {
        input.style.borderColor = 'rgba(148, 163, 184, 0.2)';
        input.style.boxShadow = 'none';
        input.value = Math.max(1, Math.round(parseFloat(input.value)) || 1);
    });
    input.addEventListener('input', () => {
        input.value = Math.round(parseFloat(input.value));
    });
    input.addEventListener('keydown', (e) => {
        const v = parseInt(input.value) || 1;
        if (e.key === 'ArrowUp') { e.preventDefault(); input.value = v + 1; if (isEnabled) applySpeed(); }
        else if (e.key === 'ArrowDown') { e.preventDefault(); input.value = Math.max(1, v - 1); if (isEnabled) applySpeed(); }
    });

    toggleButton.addEventListener('mouseover', () => {
        toggleButton.style.background = isEnabled ? '#dc2626' : '#2563eb';
        toggleButton.style.transform = 'translateY(-1px)';
    });
    toggleButton.addEventListener('mouseout', () => {
        toggleButton.style.background = isEnabled ? '#ef4444' : '#3b82f6';
        toggleButton.style.transform = 'translateY(0)';
    });
    toggleButton.addEventListener('click', () => {
        isEnabled = !isEnabled;
        toggleButton.textContent = isEnabled ? 'Disable' : 'Enable';
        toggleButton.style.background = isEnabled ? '#ef4444' : '#3b82f6';
        if (isEnabled) applySpeed();
        else restoreAll();
    });
    input.addEventListener('change', () => { if (isEnabled) applySpeed(); });

    controls.appendChild(input);
    controls.appendChild(toggleButton);
    document.body.appendChild(controls);

    // ─── Core ──────────────────────────────────────────────────────────────────

    const orig = {
        setTimeout:           window.setTimeout.bind(window),
        setInterval:          window.setInterval.bind(window),
        clearTimeout:         window.clearTimeout.bind(window),
        clearInterval:        window.clearInterval.bind(window),
        requestAnimationFrame: window.requestAnimationFrame.bind(window),
        dateNow:              Date.now.bind(Date),
        Date:                 Date,
        perfNow:              performance.now.bind(performance),
    };

    let speed = 1;
    // Wall-clock anchor for Date/performance warping
    let warpRealBase  = orig.dateNow();
    let warpVirtBase  = orig.dateNow();
    let perfRealBase  = orig.perfNow();
    let perfVirtBase  = orig.perfNow();

    // ── 1. setTimeout / setInterval ───────────────────────────────────────────
    function patchTimers() {
        window.setTimeout = (cb, delay, ...args) =>
            orig.setTimeout(cb, (delay || 0) / speed, ...args);
        window.setInterval = (cb, delay, ...args) =>
            orig.setInterval(cb, (delay || 0) / speed, ...args);
    }

    // ── 2. requestAnimationFrame ──────────────────────────────────────────────
    function patchRAF() {
        window.requestAnimationFrame = (cb) =>
            orig.requestAnimationFrame((ts) => cb(ts * speed));
    }

    // ── 3. Date.now / new Date() ──────────────────────────────────────────────
    function virtualNow() {
        const elapsed = orig.dateNow() - warpRealBase;
        return warpVirtBase + elapsed * speed;
    }

    function patchDate() {
        function FakeDate(...args) {
            if (args.length === 0) return new orig.Date(virtualNow());
            return new orig.Date(...args);
        }
        FakeDate.prototype      = orig.Date.prototype;
        FakeDate.now            = virtualNow;
        FakeDate.parse          = orig.Date.parse;
        FakeDate.UTC            = orig.Date.UTC;
        window.Date             = FakeDate;
    }

    // ── 4. performance.now() ──────────────────────────────────────────────────
    function patchPerformance() {
        const desc = Object.getOwnPropertyDescriptor(Performance.prototype, 'now');
        Object.defineProperty(performance, 'now', {
            value: function() {
                const elapsed = orig.perfNow() - perfRealBase;
                return perfVirtBase + elapsed * speed;
            },
            configurable: true,
            writable: true,
        });
    }

    // ── 5. Videos / audio (<video> and <audio>) ───────────────────────────────
    function applyMediaSpeed() {
        document.querySelectorAll('video, audio').forEach(el => {
            el.playbackRate = speed;
        });
    }

    // Watch for new media elements added after patch
    let mediaObserver = null;
    function watchNewMedia() {
        if (mediaObserver) return;
        mediaObserver = new MutationObserver((mutations) => {
            if (!isEnabled) return;
            for (const m of mutations) {
                m.addedNodes.forEach(node => {
                    if (node.nodeName === 'VIDEO' || node.nodeName === 'AUDIO') {
                        node.playbackRate = speed;
                    }
                    if (node.querySelectorAll) {
                        node.querySelectorAll('video, audio').forEach(el => {
                            el.playbackRate = speed;
                        });
                    }
                });
            }
        });
        mediaObserver.observe(document.documentElement, { childList: true, subtree: true });
    }

    // Also patch the play() method so any newly-played element gets the right rate
    const origPlay = HTMLMediaElement.prototype.play;
    HTMLMediaElement.prototype.play = function() {
        if (isEnabled) this.playbackRate = speed;
        return origPlay.call(this);
    };

    // ── 6. CSS / Web Animations ───────────────────────────────────────────────
    function applyAnimationSpeed() {
        // Web Animations API — covers CSS animations & transitions that are
        // represented as Animation objects (Chrome/Firefox/Edge)
        if (document.getAnimations) {
            document.getAnimations().forEach(anim => {
                anim.playbackRate = speed;
            });
        }

        // document.timeline.currentTime is read-only in most browsers, but
        // individual Animation.playbackRate is the right lever above.
        // As a fallback, inject a <style> that overrides animation/transition
        // durations on every element via a CSS custom property trick.
        applyAnimationCSS();
    }

    let animStyleEl = null;
    function applyAnimationCSS() {
        if (!animStyleEl) {
            animStyleEl = document.createElement('style');
            animStyleEl.id = '__wsc_anim__';
            document.head.appendChild(animStyleEl);
        }
        // Divide all declared durations by the speed factor.
        // This targets elements that don't use the Web Animations API.
        animStyleEl.textContent = `
            *, *::before, *::after {
                animation-duration:        calc(var(--wsc-dur, 1s) / ${speed}) !important;
                animation-delay:           calc(var(--wsc-del, 0s) / ${speed}) !important;
                transition-duration:       calc(var(--wsc-tdur, 0s) / ${speed}) !important;
                transition-delay:          calc(var(--wsc-tdel, 0s) / ${speed}) !important;
            }
        `;
        // Note: because most sites set literal values (not --wsc-* vars) this
        // override is imperfect for CSS-defined durations, but the Web Animations
        // playbackRate above covers the vast majority of animated content.
    }

    function removeAnimationCSS() {
        if (animStyleEl) {
            animStyleEl.textContent = '';
        }
    }

    // Re-apply Web Animations playbackRate on new animations (MutationObserver
    // doesn't catch new Animation objects, but we can poll lightly)
    let animPoller = null;
    function startAnimPoller() {
        if (animPoller) return;
        animPoller = orig.setInterval(() => {
            if (!isEnabled || !document.getAnimations) return;
            document.getAnimations().forEach(anim => {
                if (anim.playbackRate !== speed) anim.playbackRate = speed;
            });
        }, 200);
    }
    function stopAnimPoller() {
        if (animPoller) { orig.clearInterval(animPoller); animPoller = null; }
    }

    // ─── Apply / Restore ───────────────────────────────────────────────────────

    function applySpeed() {
        speed = Math.max(1, parseInt(input.value) || 1);

        // Reset time warp anchors so there's no jump
        warpRealBase = orig.dateNow();
        warpVirtBase = orig.dateNow();
        perfRealBase = orig.perfNow();
        perfVirtBase = orig.perfNow();

        patchTimers();
        patchRAF();
        patchDate();
        patchPerformance();
        applyMediaSpeed();
        watchNewMedia();
        applyAnimationSpeed();
        startAnimPoller();
    }

    function restoreAll() {
        window.setTimeout           = orig.setTimeout;
        window.setInterval          = orig.setInterval;
        window.requestAnimationFrame = orig.requestAnimationFrame;
        window.Date                 = orig.Date;
        Object.defineProperty(performance, 'now', {
            value: orig.perfNow, configurable: true, writable: true
        });

        // Restore media
        document.querySelectorAll('video, audio').forEach(el => {
            el.playbackRate = 1;
        });

        // Restore animations
        if (document.getAnimations) {
            document.getAnimations().forEach(anim => { anim.playbackRate = 1; });
        }
        removeAnimationCSS();
        stopAnimPoller();

        if (mediaObserver) { mediaObserver.disconnect(); mediaObserver = null; }

        speed = 1;
    }

})();