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