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, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

Advertisement:

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

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