YouTube Color Filter

Add customizable color filters to YouTube videos

Size

11.7 KB

Version

1.0.1

Created

Feb 10, 2026

Updated

27 days ago

1// ==UserScript==
2// @name		YouTube Color Filter
3// @description		Add customizable color filters to YouTube videos
4// @version		1.0.1
5// @match		https://*.youtube.com/*
6// @icon		https://www.youtube.com/s/desktop/2a3cb36e/img/favicon_32x32.png
7// ==/UserScript==
8(function() {
9    'use strict';
10
11    console.log('YouTube Color Filter extension loaded');
12
13    // Debounce function to prevent excessive calls
14    function debounce(func, wait) {
15        let timeout;
16        return function executedFunction(...args) {
17            const later = () => {
18                clearTimeout(timeout);
19                func(...args);
20            };
21            clearTimeout(timeout);
22            timeout = setTimeout(later, wait);
23        };
24    }
25
26    // Initialize the extension
27    async function init() {
28        console.log('Initializing YouTube Color Filter');
29        
30        // Wait for video player to be ready
31        const waitForVideo = setInterval(async () => {
32            const video = document.querySelector('video.html5-main-video');
33            if (video) {
34                clearInterval(waitForVideo);
35                console.log('Video element found');
36                await setupColorFilter(video);
37            }
38        }, 1000);
39    }
40
41    // Setup color filter controls and functionality
42    async function setupColorFilter(video) {
43        // Load saved filter settings
44        const savedFilters = await GM.getValue('youtube_color_filters', {
45            brightness: 100,
46            contrast: 100,
47            saturation: 100,
48            hue: 0,
49            sepia: 0,
50            grayscale: 0,
51            invert: 0,
52            blur: 0
53        });
54
55        console.log('Loaded saved filters:', savedFilters);
56
57        // Apply saved filters
58        applyFilters(video, savedFilters);
59
60        // Create filter control panel
61        createFilterPanel(video, savedFilters);
62
63        // Watch for video element changes (e.g., when navigating to new video)
64        observeVideoChanges();
65    }
66
67    // Apply CSS filters to video
68    function applyFilters(video, filters) {
69        const filterString = `
70            brightness(${filters.brightness}%)
71            contrast(${filters.contrast}%)
72            saturate(${filters.saturation}%)
73            hue-rotate(${filters.hue}deg)
74            sepia(${filters.sepia}%)
75            grayscale(${filters.grayscale}%)
76            invert(${filters.invert}%)
77            blur(${filters.blur}px)
78        `.trim();
79        
80        video.style.filter = filterString;
81        console.log('Applied filters:', filterString);
82    }
83
84    // Create the filter control panel UI
85    function createFilterPanel(video, initialFilters) {
86        // Remove existing panel if any
87        const existingPanel = document.getElementById('yt-color-filter-panel');
88        if (existingPanel) {
89            existingPanel.remove();
90        }
91
92        // Create panel container
93        const panel = document.createElement('div');
94        panel.id = 'yt-color-filter-panel';
95        panel.style.cssText = `
96            position: fixed;
97            top: 80px;
98            right: 20px;
99            background: rgba(0, 0, 0, 0.9);
100            color: white;
101            padding: 20px;
102            border-radius: 8px;
103            z-index: 9999;
104            font-family: 'Roboto', Arial, sans-serif;
105            font-size: 14px;
106            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
107            max-width: 300px;
108            display: none;
109        `;
110
111        // Create panel header
112        const header = document.createElement('div');
113        header.style.cssText = `
114            display: flex;
115            justify-content: space-between;
116            align-items: center;
117            margin-bottom: 15px;
118            padding-bottom: 10px;
119            border-bottom: 1px solid rgba(255, 255, 255, 0.2);
120        `;
121        header.innerHTML = `
122            <span style="font-weight: bold; font-size: 16px;">Color Filters</span>
123            <button id="yt-filter-close" style="background: none; border: none; color: white; cursor: pointer; font-size: 20px; padding: 0; width: 24px; height: 24px;">×</button>
124        `;
125        panel.appendChild(header);
126
127        // Filter controls
128        const filters = [
129            { name: 'brightness', label: 'Brightness', min: 0, max: 200, step: 1, unit: '%' },
130            { name: 'contrast', label: 'Contrast', min: 0, max: 200, step: 1, unit: '%' },
131            { name: 'saturation', label: 'Saturation', min: 0, max: 200, step: 1, unit: '%' },
132            { name: 'hue', label: 'Hue Rotate', min: 0, max: 360, step: 1, unit: '°' },
133            { name: 'sepia', label: 'Sepia', min: 0, max: 100, step: 1, unit: '%' },
134            { name: 'grayscale', label: 'Grayscale', min: 0, max: 100, step: 1, unit: '%' },
135            { name: 'invert', label: 'Invert', min: 0, max: 100, step: 1, unit: '%' },
136            { name: 'blur', label: 'Blur', min: 0, max: 10, step: 0.1, unit: 'px' }
137        ];
138
139        const currentFilters = { ...initialFilters };
140
141        filters.forEach(filter => {
142            const controlGroup = document.createElement('div');
143            controlGroup.style.cssText = 'margin-bottom: 12px;';
144
145            const labelRow = document.createElement('div');
146            labelRow.style.cssText = 'display: flex; justify-content: space-between; margin-bottom: 5px;';
147            
148            const label = document.createElement('label');
149            label.textContent = filter.label;
150            label.style.cssText = 'font-size: 13px;';
151            
152            const valueDisplay = document.createElement('span');
153            valueDisplay.id = `yt-filter-value-${filter.name}`;
154            valueDisplay.textContent = `${currentFilters[filter.name]}${filter.unit}`;
155            valueDisplay.style.cssText = 'font-size: 13px; color: #aaa;';
156            
157            labelRow.appendChild(label);
158            labelRow.appendChild(valueDisplay);
159            
160            const slider = document.createElement('input');
161            slider.type = 'range';
162            slider.min = filter.min;
163            slider.max = filter.max;
164            slider.step = filter.step;
165            slider.value = currentFilters[filter.name];
166            slider.style.cssText = `
167                width: 100%;
168                cursor: pointer;
169                accent-color: #ff0000;
170            `;
171
172            const debouncedSave = debounce(async (filters) => {
173                await GM.setValue('youtube_color_filters', filters);
174                console.log('Saved filters:', filters);
175            }, 500);
176
177            slider.addEventListener('input', (e) => {
178                const value = parseFloat(e.target.value);
179                currentFilters[filter.name] = value;
180                valueDisplay.textContent = `${value}${filter.unit}`;
181                applyFilters(video, currentFilters);
182                debouncedSave(currentFilters);
183            });
184
185            controlGroup.appendChild(labelRow);
186            controlGroup.appendChild(slider);
187            panel.appendChild(controlGroup);
188        });
189
190        // Reset button
191        const resetButton = document.createElement('button');
192        resetButton.textContent = 'Reset All';
193        resetButton.style.cssText = `
194            width: 100%;
195            padding: 10px;
196            margin-top: 15px;
197            background: #ff0000;
198            color: white;
199            border: none;
200            border-radius: 4px;
201            cursor: pointer;
202            font-size: 14px;
203            font-weight: bold;
204        `;
205        resetButton.addEventListener('click', async () => {
206            const defaultFilters = {
207                brightness: 100,
208                contrast: 100,
209                saturation: 100,
210                hue: 0,
211                sepia: 0,
212                grayscale: 0,
213                invert: 0,
214                blur: 0
215            };
216            
217            // Update all sliders and values
218            filters.forEach(filter => {
219                const slider = panel.querySelector(`input[type="range"][value="${currentFilters[filter.name]}"]`);
220                const valueDisplay = document.getElementById(`yt-filter-value-${filter.name}`);
221                if (slider) {
222                    slider.value = defaultFilters[filter.name];
223                }
224                if (valueDisplay) {
225                    valueDisplay.textContent = `${defaultFilters[filter.name]}${filter.unit}`;
226                }
227                currentFilters[filter.name] = defaultFilters[filter.name];
228            });
229            
230            applyFilters(video, defaultFilters);
231            await GM.setValue('youtube_color_filters', defaultFilters);
232            console.log('Reset filters to default');
233        });
234        panel.appendChild(resetButton);
235
236        // Add panel to page
237        document.body.appendChild(panel);
238
239        // Close button functionality
240        document.getElementById('yt-filter-close').addEventListener('click', () => {
241            panel.style.display = 'none';
242        });
243
244        // Create toggle button
245        createToggleButton(panel);
246
247        console.log('Filter panel created');
248    }
249
250    // Create toggle button to show/hide panel
251    function createToggleButton(panel) {
252        // Remove existing button if any
253        const existingButton = document.getElementById('yt-color-filter-toggle');
254        if (existingButton) {
255            existingButton.remove();
256        }
257
258        const toggleButton = document.createElement('button');
259        toggleButton.id = 'yt-color-filter-toggle';
260        toggleButton.innerHTML = '🎨';
261        toggleButton.title = 'Color Filters';
262        toggleButton.style.cssText = `
263            position: fixed;
264            top: 80px;
265            right: 20px;
266            width: 48px;
267            height: 48px;
268            background: rgba(0, 0, 0, 0.8);
269            color: white;
270            border: 2px solid rgba(255, 255, 255, 0.3);
271            border-radius: 50%;
272            cursor: pointer;
273            font-size: 24px;
274            z-index: 9998;
275            display: flex;
276            align-items: center;
277            justify-content: center;
278            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
279            transition: all 0.2s ease;
280        `;
281
282        toggleButton.addEventListener('mouseenter', () => {
283            toggleButton.style.background = 'rgba(255, 0, 0, 0.8)';
284            toggleButton.style.transform = 'scale(1.1)';
285        });
286
287        toggleButton.addEventListener('mouseleave', () => {
288            toggleButton.style.background = 'rgba(0, 0, 0, 0.8)';
289            toggleButton.style.transform = 'scale(1)';
290        });
291
292        toggleButton.addEventListener('click', () => {
293            if (panel.style.display === 'none') {
294                panel.style.display = 'block';
295            } else {
296                panel.style.display = 'none';
297            }
298        });
299
300        document.body.appendChild(toggleButton);
301        console.log('Toggle button created');
302    }
303
304    // Observe video element changes for navigation
305    function observeVideoChanges() {
306        const observer = new MutationObserver(debounce(async () => {
307            const video = document.querySelector('video.html5-main-video');
308            if (video && !video.style.filter) {
309                console.log('Video changed, reapplying filters');
310                const savedFilters = await GM.getValue('youtube_color_filters', {
311                    brightness: 100,
312                    contrast: 100,
313                    saturation: 100,
314                    hue: 0,
315                    sepia: 0,
316                    grayscale: 0,
317                    invert: 0,
318                    blur: 0
319                });
320                applyFilters(video, savedFilters);
321            }
322        }, 1000));
323
324        observer.observe(document.body, {
325            childList: true,
326            subtree: true
327        });
328
329        console.log('Video observer started');
330    }
331
332    // Start the extension
333    init();
334})();