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

})();