Youtube Video Filters

Ehance Youtube video player with filters

As of 2024-05-24. See the latest version.

// ==UserScript==
// @name 	Youtube Video Filters   
// @author 	Xavier Bouhours
// @version 	2024-05-24
// @description     Ehance Youtube video player with filters
// @description:fr 	Plugin pour page Youtube qui ajoute un panneau de réglages de filtres au player des vidéos. La propriété css filter est exploitée pour le rendu.
// @run-at       	document-end
// @compatible 	firefox
// @license	MIT
// @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 pour les filtres
		fx 		: D.createElement("button"),

 		// On crée le panneau de manipulation des filtres
 		// @see: https://developer.mozilla.org/fr/docs/Web/HTML/Element/dialog
 		// panel 	: D.createElement("dialog"),
 		panel 	: D.createElement("div"),
 		header	: D.createElement("header"),


 		// @ex: <button autofocus>Fermer</button>
 		close 	: D.createElement("button"),
 	
 		// 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 }
					//rotate	: { min:-180, val:0, max:180, unit:"deg", step: 90 }
 				},

 		// On crée la fenêtre des filtres
		refresh : function()
				{
					var k, f = "";

					// On compile chaque filtre
					for( k in V.filters )
					{
						var filter = V.filters[k];
						f+= k+'('+filter.val+filter.unit+') ';
					};

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

		// 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 
					I = D.createElement("input"), 
					L = D.createElement("label"),
					C = D.createElement("div");
					
				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 label
				C.append(L);
					L.append(name);
					// Capitalize first letter
					// L.append(name[0].toUpperCase() + name.slice(1));

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

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

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

		init	: function()
				{
					// 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.showModal(); // only if panel is a "dialog" tag
	  						V.panel.classList.remove(V.panel.classList.item(1)); // 0=.video-filters 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 filter = V.filters[n];
							V.addfilter( n, filter.min, filter.val, filter.max, filter.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.close(); // only if panel is a "dialog" tag
								V.panel.classList.add("hidden");
							});

					// On installe le theme
					V.panel.append( V.mkstyles() );
					// … et on ajoute la fonctionnalité de déplacement
					V.draggable(V.panel);
				},

		// Optionnel :
		
		// On injecte des styles via js
		theme 	: `	/* Couleurs du theme en accord avec navigateur */
					:root
					{
						--vf-back-r: 255;
						--vf-back-v: 255;
						--vf-back-b: 255;
						--vf-back: rgb( var(--vf-back-r), var(--vf-back-v), var(--vf-back-b) );
						--vf-txt: black
					}
					@media (prefers-color-scheme: dark)
					{
						:root
						{
					    	--vf-back-r: 0;
							--vf-back-v: 0;
							--vf-back-b: 0;
					    	--vf-txt: white
					    }
					}

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

					/* On améliore la boite de dialog */
					.video-filters
					{
						position: absolute;
						z-index: 1000;
						width: 80vw;
						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 .3em var(--vf-txt)
					}
					.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);
							color: var(--vf-txt);
							width:100%
						}

						/* On positionne les conteneurs pour chaque filtre */
						.video-filters>div
						{
							box-sizing: border-box;
							margin-bottom: 1em;
							max-width: 20em
						}

						/* 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
						}

						/* On positionne le bouton de fermeture */
						.video-filters .close
						{
							display: block ;
							position: absolute ;
							top: .2em ;
							right: .2em
						}`,

		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 the DIV 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;
				//dragZone = V.header ; //el.querySelector("header");

			//dragZone.onmousedown = startDrag;
			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 = null;
				D.onmousemove = null;
			}
		}
}

})( window, document );


// Désactiver la condition suivante (if doc… youtube) pour en faire un script "greasemonkey"
// @see: https://addons.mozilla.org/fr/firefox/addon/greasemonkey/

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

// @todo: on-of for all FX, extend to other and multiple player, test svg filter…