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