Web Speed Controller

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

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey to install this script.

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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;
    }

})();