Universal Video Image Control

On-screen slider control panel for Gamma, Brightness, Contrast, and Saturation with cross-origin iframe sync, icon toggle, active switch, isolated Shadow DOM, custom track gradients, and dimmed inactive states.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

// ==UserScript==
// @name         Universal Video Image Control
// @namespace    https://github.com/
// @version      3.0
// @description  On-screen slider control panel for Gamma, Brightness, Contrast, and Saturation with cross-origin iframe sync, icon toggle, active switch, isolated Shadow DOM, custom track gradients, and dimmed inactive states.
// @match        *://*/*
// @allFrames    true
// @run-at       document-end
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    const S_GAMMA = 'vid_ctrl_gamma';
    const S_BRIGHT = 'vid_ctrl_bright';
    const S_CONTRAST = 'vid_ctrl_contrast';
    const S_SATURATE = 'vid_ctrl_saturate';
    const S_ACTIVE = 'vid_ctrl_active';

    let vals = {
        gamma: parseFloat(localStorage.getItem(S_GAMMA)) || 1.0,
        brightness: parseFloat(localStorage.getItem(S_BRIGHT)) || 1.0,
        contrast: parseFloat(localStorage.getItem(S_CONTRAST)) || 1.0,
        saturation: parseFloat(localStorage.getItem(S_SATURATE)) || 1.0,
        active: localStorage.getItem(S_ACTIVE) !== 'false'
    };

    let container = null;
    let icon = null;
    let panel = null;
    let hideTimeout = null;
    let isHovering = false;
    let isPanelOpen = false;

    // --- Cross-Origin Sync Logic ---
    function broadcastSync() {
        localStorage.setItem(S_GAMMA, vals.gamma);
        localStorage.setItem(S_BRIGHT, vals.brightness);
        localStorage.setItem(S_CONTRAST, vals.contrast);
        localStorage.setItem(S_SATURATE, vals.saturation);
        localStorage.setItem(S_ACTIVE, vals.active);

        const payload = { app: 'VidCtrl', action: 'sync_down', vals: vals };

        if (window !== window.top) {
            window.top.postMessage({ app: 'VidCtrl', action: 'sync_up', vals: vals }, '*');
        } else {
            document.querySelectorAll('iframe').forEach(ifr => {
                try { ifr.contentWindow.postMessage(payload, '*'); } catch(e){}
            });
        }
    }

    window.addEventListener('message', (e) => {
        if (!e.data || e.data.app !== 'VidCtrl') return;

        if (e.data.action === 'request_sync' && window === window.top) {
            if (e.source) e.source.postMessage({ app: 'VidCtrl', action: 'sync_down', vals: vals }, '*');
        }
        else if (e.data.action === 'sync_up' && window === window.top) {
            vals = Object.assign({}, e.data.vals);
            localStorage.setItem(S_GAMMA, vals.gamma);
            localStorage.setItem(S_BRIGHT, vals.brightness);
            localStorage.setItem(S_CONTRAST, vals.contrast);
            localStorage.setItem(S_SATURATE, vals.saturation);
            localStorage.setItem(S_ACTIVE, vals.active);

            applyFilters();
            updateUI();

            document.querySelectorAll('iframe').forEach(ifr => {
                if (ifr.contentWindow !== e.source) {
                    try { ifr.contentWindow.postMessage({ app: 'VidCtrl', action: 'sync_down', vals: vals }, '*'); } catch(err){}
                }
            });
        }
        else if (e.data.action === 'sync_down') {
            vals = Object.assign({}, e.data.vals);
            applyFilters();
            updateUI();

            document.querySelectorAll('iframe').forEach(ifr => {
                try { ifr.contentWindow.postMessage(e.data, '*'); } catch(err){}
            });
        }
    });

    if (window !== window.top) {
        window.top.postMessage({ app: 'VidCtrl', action: 'request_sync' }, '*');
    }
    // -------------------------------

    function updateSliderTrack(el) {
        if (!el) return;
        const min = parseFloat(el.min) || 0;
        const max = parseFloat(el.max) || 100;
        const val = parseFloat(el.value) || 0;
        const pct = ((val - min) / (max - min)) * 100;

        // Multi-layered background: semi-transparent red layer on top of the base dark gray gradient track (dark to light)
        el.style.setProperty('background', `linear-gradient(to right, rgba(255, 51, 51, 0.5) 0%, rgba(255, 51, 51, 0.5) ${pct}%, transparent ${pct}%, transparent 100%), linear-gradient(to right, #1a1a1a 0%, #2f2f2f 33.5%, #444444 100%)`, 'important');    }

    function createUI() {
        if (document.getElementById('video-img-ctrl-container')) {
            container = document.getElementById('video-img-ctrl-container');
            return;
        }

        container = document.createElement('div');
        container.id = 'video-img-ctrl-container';

        container.style.cssText = `
            position: fixed !important;
            top: 10px !important;
            right: 10px !important;
            z-index: 2147483647 !important;
            opacity: 0 !important;
            pointer-events: none !important;
            transition: opacity 0.3s ease-in-out !important;
        `;

        const shadow = container.attachShadow({ mode: 'open' });

        const style = document.createElement('style');
        style.textContent = `
            :host {
                display: flex !important;
                flex-direction: column !important;
                align-items: flex-end !important;
                gap: 8px !important;
            }
            .vid-ctrl-icon {
                background: rgba(0, 0, 0, 0.80) !important;
                backdrop-filter: blur(4px) !important;
                color: #fff !important;
                width: 32px !important;
                height: 32px !important;
                border-radius: 6px !important;
                display: flex !important;
                justify-content: center !important;
                align-items: center !important;
                cursor: pointer !important;
                box-shadow: 0 4px 15px rgba(0,0,0,0.8) !important;
            }
            .vid-ctrl-panel {
                background: rgba(0, 0, 0, 0.80) !important;
                backdrop-filter: blur(4px) !important;
                color: #fff !important;
                padding: 10px !important;
                border-radius: 6px !important;
                font-family: system-ui, -apple-system, sans-serif !important;
                font-size: 14px !important;
                box-shadow: 0 4px 15px rgba(0,0,0,0.8) !important;
                border: none !important;
                display: none !important;
                flex-direction: column !important;
                gap: 8px !important;
                user-select: none !important;
                width: 440px !important;
                box-sizing: border-box !important;
            }
            .row {
                display: flex !important;
                justify-content: space-between !important;
                align-items: center !important;
                width: 100% !important;
                box-sizing: border-box !important;
                transition: opacity 0.2s ease !important;
            }
            label {
                margin: 0 !important;
                font-weight: 500 !important;
                color: #fff !important;
                text-align: left !important;
                font-size: 14px !important;
                flex-shrink: 0 !important;
            }
            .label-slider { width: 85px !important; }
            .span-val {
                min-width: 40px !important;
                text-align: right !important;
                font-weight: 500 !important;
                color: #fff !important;
                font-size: 14px !important;
                flex-shrink: 0 !important;
            }
            .vid-ctrl-slider {
                -webkit-appearance: none !important;
                appearance: none !important;
                flex-grow: 1 !important;
                margin: 0 8px !important;
                cursor: pointer !important;
                min-width: 0 !important;
                box-sizing: border-box !important;
                height: 6px !important;
                border-radius: 3px !important;
                outline: none !important;
            }
            .vid-ctrl-slider::-webkit-slider-thumb {
                -webkit-appearance: none !important;
                appearance: none !important;
                width: 14px !important;
                height: 14px !important;
                border-radius: 50% !important;
                background: #ff3333 !important;
                cursor: pointer !important;
                border: none !important;
            }
            .vid-ctrl-slider::-moz-range-thumb {
                width: 14px !important;
                height: 14px !important;
                border-radius: 50% !important;
                background: #ff3333 !important;
                cursor: pointer !important;
                border: none !important;
            }
            .footer {
                display: flex !important;
                justify-content: space-between !important;
                align-items: center !important;
                width: 100% !important;
                margin-top: 4px !important;
                box-sizing: border-box !important;
            }
            .footer-label {
                display: flex !important;
                align-items: center !important;
                gap: 6px !important;
                cursor: pointer !important;
                user-select: none !important;
                font-size: 14px !important;
            }
            .vid-ctrl-checkbox {
                accent-color: #ff3333 !important;
                margin: 0 !important;
                cursor: pointer !important;
                width: 15px !important;
                height: 15px !important;
            }
            #ctrl-reset {
                background: #333 !important;
                color: #fff !important;
                border: 1px solid #555 !important;
                padding: 4px 16px !important;
                border-radius: 4px !important;
                cursor: pointer !important;
                font-size: 13px !important;
                font-weight: 500 !important;
                box-sizing: border-box !important;
                transition: opacity 0.2s ease !important;
            }
            /* Visual gray-out state rules when filters are inactive */
            .vid-ctrl-panel.is-inactive .row,
            .vid-ctrl-panel.is-inactive #ctrl-reset {
                opacity: 0.35 !important;
                pointer-events: none !important;
            }
        `;
        shadow.appendChild(style);

        icon = document.createElement('div');
        icon.className = 'vid-ctrl-icon';
        icon.innerHTML = `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="21" x2="4" y2="14"></line><line x1="4" y1="10" x2="4" y2="3"></line><line x1="12" y1="21" x2="12" y2="12"></line><line x1="12" y1="8" x2="12" y2="3"></line><line x1="20" y1="21" x2="20" y2="16"></line><line x1="20" y1="12" x2="20" y2="3"></line><line x1="1" y1="14" x2="7" y2="14"></line><line x1="9" y1="8" x2="15" y2="8"></line><line x1="17" y1="16" x2="23" y2="16"></line></svg>`;

        panel = document.createElement('div');
        panel.className = 'vid-ctrl-panel';
        if (!vals.active) panel.classList.add('is-inactive');

        const createRow = (id, label, min, max, val) => `
            <div class="row">
                <label for="${id}" class="label-slider">${label}</label>
                <input type="range" id="${id}" class="vid-ctrl-slider" min="${min}" max="${max}" step="0.05" value="${val}">
                <span id="${id}-val" class="span-val">${val.toFixed(2)}</span>
            </div>
        `;

        panel.innerHTML = `
            ${createRow('ctrl-gamma', 'Gamma', 0.5, 2.0, vals.gamma)}
            ${createRow('ctrl-bright', 'Brightness', 0.5, 2.0, vals.brightness)}
            ${createRow('ctrl-contrast', 'Contrast', 0.5, 2.0, vals.contrast)}
            ${createRow('ctrl-saturate', 'Saturation', 0.0, 3.0, vals.saturation)}
            <div class="footer">
                <label class="footer-label">
                    <input type="checkbox" id="ctrl-active" class="vid-ctrl-checkbox" ${vals.active ? 'checked' : ''}>
                    Active
                </label>
                <button id="ctrl-reset">Reset All</button>
            </div>
        `;

        shadow.appendChild(icon);
        shadow.appendChild(panel);

        panel.querySelectorAll('.vid-ctrl-slider').forEach(updateSliderTrack);

        attachUI();
        bindEvents();
    }

    function bindEvents() {
        icon.addEventListener('click', () => {
            isPanelOpen = !isPanelOpen;
            panel.style.setProperty('display', isPanelOpen ? 'flex' : 'none', 'important');
        });

        panel.querySelector('#ctrl-active').addEventListener('change', (e) => {
            vals.active = e.target.checked;
            if (vals.active) {
                panel.classList.remove('is-inactive');
            } else {
                panel.classList.add('is-inactive');
            }
            applyFilters();
            broadcastSync();
        });

        const inputs = [
            { id: 'ctrl-gamma', key: 'gamma' },
            { id: 'ctrl-bright', key: 'brightness' },
            { id: 'ctrl-contrast', key: 'contrast' },
            { id: 'ctrl-saturate', key: 'saturation' }
        ];

        inputs.forEach(item => {
            const el = panel.querySelector(`#${item.id}`);
            const display = panel.querySelector(`#${item.id}-val`);

            el.addEventListener('input', (e) => {
                vals[item.key] = parseFloat(e.target.value);
                display.textContent = vals[item.key].toFixed(2);
                updateSliderTrack(el);
                applyFilters();
                broadcastSync();
            });
        });

        panel.querySelector('#ctrl-reset').addEventListener('click', () => {
            vals = { gamma: 1.0, brightness: 1.0, contrast: 1.0, saturation: 1.0, active: true };
            updateUI();
            applyFilters();
            broadcastSync();
        });

        container.addEventListener('mouseenter', () => {
            isHovering = true;
            showUI();
        });

        container.addEventListener('mouseleave', () => {
            isHovering = false;
            queueHide();
        });
    }

    function updateUI() {
        if (!panel) return;

        if (vals.active) {
            panel.classList.remove('is-inactive');
        } else {
            panel.classList.add('is-inactive');
        }

        const inputs = [
            { id: 'ctrl-gamma', key: 'gamma' },
            { id: 'ctrl-bright', key: 'brightness' },
            { id: 'ctrl-contrast', key: 'contrast' },
            { id: 'ctrl-saturate', key: 'saturation' }
        ];
        inputs.forEach(item => {
            const el = panel.querySelector(`#${item.id}`);
            const display = panel.querySelector(`#${item.id}-val`);
            if (el && display) {
                el.value = vals[item.key];
                display.textContent = vals[item.key].toFixed(2);
                updateSliderTrack(el);
            }
        });
        const activeCb = panel.querySelector('#ctrl-active');
        if (activeCb) {
            activeCb.checked = vals.active;
        }
    }

    function attachUI() {
        if (!container) return;
        const target = document.fullscreenElement || document.webkitFullscreenElement || document.documentElement || document.body;
        if (container.parentNode !== target) {
            target.appendChild(container);
        }
    }

    function applyFilters() {
        const videos = document.querySelectorAll('video');

        if (!vals.active) {
            videos.forEach(video => {
                video.style.setProperty('filter', 'none', 'important');
            });
            return;
        }

        const b = vals.gamma * vals.brightness;
        const c = (1 / vals.gamma) * vals.contrast;
        const s = vals.saturation;

        videos.forEach(video => {
            video.style.setProperty('filter', `brightness(${b}) contrast(${c}) saturate(${s})`, 'important');
        });
    }

    function showUI() {
        if (!container) return;
        container.style.setProperty('opacity', '1.0', 'important');
        container.style.setProperty('pointer-events', 'auto', 'important');
        clearTimeout(hideTimeout);
        if (!isHovering) {
            queueHide();
        }
    }

    function queueHide() {
        clearTimeout(hideTimeout);
        hideTimeout = setTimeout(() => {
            if (container && !isHovering) {
                container.style.setProperty('opacity', '0', 'important');
                container.style.setProperty('pointer-events', 'none', 'important');

                if (isPanelOpen) {
                    isPanelOpen = false;
                    panel.style.setProperty('display', 'none', 'important');
                }
            }
        }, 3000);
    }

    document.addEventListener('mousemove', showUI);
    document.addEventListener('fullscreenchange', attachUI);
    document.addEventListener('webkitfullscreenchange', attachUI);

    const observer = new MutationObserver(() => {
        if (document.querySelector('video')) {
            createUI();
            applyFilters();
            attachUI();
        }
    });

    observer.observe(document.body || document.documentElement, { childList: true, subtree: true });

    if (document.querySelector('video')) {
        createUI();
        applyFilters();
    }
})();