Youtube Video Filters

Ehance Youtube video player with filters (see "FX" button near "Fullscreen")

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name 		Youtube Video Filters   
// @author 		Xavier Bouhours
// @version 		2024-05-25
// @description     	Ehance Youtube video player with filters (see "FX" button near "Fullscreen")
// @description:fr 	Ajout de filtres vidéos au player. La propriété css filter est exploitée pour le rendu.
// @description:en 	Luminosity and contrast for Youtube site player.
// @run-at       	document-end
// @compatible 		firefox
// @license		MIT
// @homepage 		https://greasyfork.org/fr/scripts/495951-youtube-video-filters
// @match        	https://*.youtube.com/*
// @namespace https://greasyfork.org/users/1306337
// ==/UserScript==


(function(W,D,undefined)
{
	const V = W.videoFx =
	{
		// On récupère le player html5 de Youtube
 		video 	: D.querySelector("video.html5-main-video"),
 		controls: D.querySelector(".ytp-right-controls"),

 		// On crée le bouton d'appel de la fenêtre des filtres
		fx 		: D.createElement("button"),

 		// On crée le panneau de manipulation des filtres
 		panel 	: D.createElement("div"),
 		header	: D.createElement("header"),

 		// On crée le set de boutons
 		close 	: D.createElement("button"),
 		fxswitch: D.createElement("input"),
 	
 		// Filters store : On initialise les filtres
 		filters : 
 				{ 
 					brightness	: { min:0, val:100, max:200, unit:"%", step: 1 },
 					contrast	: { min:0, val:100, max:200, unit:"%", step: 1 },
 					saturate	: { min:0, val:100, max:300, unit:"%", step: 1 },
					grayscale	: { min:0, val:0, max:100, unit:"%", step: 1 },
					'hue-rotate': { min:-360, val:0, max:360, unit:"deg", step: 1 },
					invert		: { min:0, val:0, max:100, unit:"%", step: 1 },
					sepia		: { min:0, val:0, max:100, unit:"%", step: 1 },
					blur		: { min:0, val:0, max:10, unit:"px", step: 1 }
 				},

 		// Application des filtres à la vidéo
		refresh : function()
				{
					var k, c = "";

					// On compile chaque filtre pour la propriété css
					for( k in V.filters )
					{
						var f = V.filters[k];
						c+= k+'('+ f.val + f.unit +') ';
					};

					// On applique le style à la vidéo
					V.video.style.filter = c ;
					// console.log(c);
				},

		// Ajouter un réglage de filtre au panneau
		// @see: https://blog.hubspot.com/website/html-slider
		// @ex: <input type='range' min='0' value='1' max='3' name='contrast'>
		addfilter : function( name, min, val, max )
		{
				let 
					C = D.createElement("div"), 	// Conteneur
					I = D.createElement("input"),	// Slider
					L = D.createElement("label");	// Titre
					
				I.type 	= "range"; 
				I.name 	= I.title = I.style.content = name;
				I.min 	= min;
				I.value	= val;
				I.max	= max;
				
				// On ajoute le conteneur pour le label et le curseur
				V.panel.append(C);

				// On ajoute le titre
				C.append(L);
					L.append(name);

				//… et le curseur de réglage
				C.append(I);

				I.addEventListener("change", function()
				{
					// On sauvegarde la valeur
					// console.log(this,I);
					V.filters[I.name].val = I.value ;

					// On applique les modifications
					V.refresh();
				});
		},

		initialized : false,

		// Initialisation du module
		init	: function()
				{
					if(!!V.initialized) return true ;

					// On ajoute le bouton "FX" aux contrôles du player
					V.controls.append(V.fx);
						V.fx.classList.add("ytp-button");
						V.fx.classList.add("video-fx");
						V.fx.append("FX");
						V.fx.addEventListener("click", () => 
						{
	  						V.panel.classList.remove(V.panel.classList.item(1)); // item(0) = "video-filters" ; item(1) = "hidden"
						});

					// On ajoute le panneau des filtres…
					// V.controls.append(V.panel);
					D.querySelector("body").append(V.panel);
					V.panel.classList.add("video-filters");
					V.panel.classList.add("hidden");
					
						// … avec son header (draggable zone)
						V.panel.append(V.header);
							V.header.append("Video Filters");

						// … avec ses filtres,
						var n ;
						for( n in V.filters )
						{
							var f = V.filters[n];
							V.addfilter( n, f.min, f.val, f.max, f.step );
						};

						// … et son bouton de fermeture
						V.header.append(V.close);
							V.close.append("X");
							V.close.setAttribute("title","Close");
							V.close.classList.add("close");
							V.close.addEventListener("click", () => 
							{
	  							V.panel.classList.add("hidden");
							});

						// … et le bouton switch dpour la désactivation des filtres
						V.header.append(V.fxswitch);
							V.fxswitch.classList.add("switch");
							V.fxswitch.setAttribute("type","checkbox");
							V.fxswitch.setAttribute("checked","true");
							V.fxswitch.addEventListener("click", () => 
							{
	  							// set On
	  							if( V.fxswitch.checked == 1 )
	  							{
	  								V.refresh();
	  								V.panel.setAttribute("class","video-filters"); // restore
	  							}
	  							// set Of
	  							else
	  							{
	  								V.video.style.filter = "none";
	  								V.panel.classList.add("of");
	  							}
							});


					// On installe le theme
					V.panel.append( V.mkstyles() );

					// … et on ajoute la fonctionnalité de déplacement
					V.draggable(V.panel);

					V.initialized = true ;
				},
		
		// UI theme
		theme 	: `	/* Couleurs du theme en accord avec navigateur */
					:root
					{
						--vf-back-r: 255;
						--vf-back-v: 255;
						--vf-back-b: 255;
						--vf-back-c: rgb( var(--vf-back-r), var(--vf-back-v), var(--vf-back-b) );
						--vf-txt-c: black;
						--vf-active-c: #3e94f6; /* ff def value */
					}
					@media (prefers-color-scheme: dark)
					{
						:root
						{
					    	--vf-back-r: 0;
							--vf-back-v: 0;
							--vf-back-b: 0;
					    	--vf-txt-c: white
					    }
					}

					/* On positionne le bouton "FX" */
					button.video-fx
					{
						transform: translateY(-1.5em)
					}

					/* On optimise le panneau des filtres */
					.video-filters
					{
						position: absolute;
						z-index: 1000;
						width: clamp(30em,30vw,100%);
						background: rgba(var(--vf-back-r), var(--vf-back-v), var(--vf-back-b), .4 );
						backdrop-filter: blur(.3em) brightness(50%);
						border-radius: 1em;
						display: flex;
						justify-content: space-evenly;
						flex-wrap: wrap;
						top: 40vh;
						left: 10vw;
						font-weight: bold;
						margin-bottom: 1em;
						overflow: hidden;
						box-shadow: 0 0 .4em var(--vf-txt-c)
					}
					.video-filters.hidden
					{
						display:none
					}

						/* On ajoute la zone de drag */
						.video-filters header
						{
							text-align: center;
							padding: 1em;
							cursor: move;
							z-index: +1;
							background: var(--vf-back-c);
							color: var(--vf-txt-c);
							width:100%
						}

						/* On positionne les conteneurs pour chaque filtre */
						.video-filters > div
						{
							box-sizing: border-box;
							margin-bottom: 1em;
							max-width: 20em;
							transition:	all .5s ease-out
						}
						.video-filters.of > div
						{
							user-select: none;
  							opacity: .4;
  							filter: grayscale(100%) blur(1px);
							pointer-events:none;
						}

							/* On positionne les filtres */
							.video-filters label,
							.video-filters input
							{
								display: block;
								width: 100%
							}

							/* On règle les intitulés des filtres */
							.video-filters label
							{
								transform: translate(1em,0);
								color: white;
								text-transform: capitalize
							}

						/* Boutons, etc */
            			.video-filters .close,
            			input[type='checkbox'].switch
            			{
	           				border-radius: 1em;
							width:1em;
							height: 1em;
							border: 2px solid var(--vf-txt-c)
            			}

						/* On positionne le bouton de fermeture */
						.video-filters .close
						{
							display: block ;
							position: absolute ;
							top: .7em ;
							right: .7em;
							line-height: 0;
							color:transparent;
							font-size: 1.3em;
						}

						/* On crée le bouton switch pour les filtres */
						input[type='checkbox'].switch
						{
							display: inline-block;
							appearance:none;
							text-align: center;
							font-size: 2em;
							background: #777;
							transform: translate(0em,.2em) ;
							box-shadow: 0 0 0 var(--vf-txt-c), .3em 0 0 var(--vf-txt-c), .6em 0 0 var(--vf-txt-c);
						}
						input[type='checkbox'].switch:checked
						{
							transform: translate(1em,.2em) ;
							box-shadow: 0 0 0 var(--vf-txt-c), -.3em 0 0 var(--vf-txt-c), -.6em 0 0 var(--vf-txt-c);
							background: var(--vf-active-c)
						}
						`,
		// Constructeur pour la feuille de styles
		mkstyles : function()
				{
					const s = D.createElement('style');
					s.type = 'text/css';
					s.id = 'video-fx-theme';
					s.innerHTML = V.theme;
					return s;
					// D.getElementsByTagName('head')[0].appendChild(s);
				},


		// Make element draggable
		// @see https://stackoverflow.com/questions/65022204/make-a-popup-modal-dialog-movable-draggable-once-it-has-appeared
		draggable: function(el)
		{
			var
				p1 = p2 = p3 = p4 = 0;

			// el.querySelector("header")
			V.header.onmousedown = startDrag;

			function startDrag(e)
			{
				e = e || W.event;
				e.preventDefault();
				// Get the mouse cursor position at startup
				p3 = e.clientX;
				p4 = e.clientY;
				D.onmouseup = stopDrag;
				// Call a function whenever the cursor moves
				D.onmousemove = drag;
			}

			function drag(e)
			{
				e = e || W.event;
				e.preventDefault();
				// Calculate the new cursor position
				p1 = p3 - e.clientX;
				p2 = p4 - e.clientY;
				p3 = e.clientX;
				p4 = e.clientY;
				// Set the element's new position:
				el.style.top = (el.offsetTop - p2) + "px";
				el.style.left = (el.offsetLeft - p1) + "px";
			}

			function stopDrag()
			{
				// Stop moving when mouse button is released
				D.onmouseup = D.onmousemove = null;
			}
		}
}

})( window, document );


// Appliquer
// @see: https://addons.mozilla.org/fr/firefox/addon/greasemonkey/

if(document.location.host==="www.youtube.com")
	videoFx.init();

// @todo: extend to embed and other player