Greasy Fork is available in English.
Add controls to pan and zoom HTML5 video.
// ==UserScript== // @name YouTube HTML5 Video Pan And Zoom // @namespace YouTubeHTML5VideoPanAndZoom // @version 1.1.9 // @license AGPLv3 // @author jcunews // @description Add controls to pan and zoom HTML5 video. // @include https://www.youtube.com/watch* // @grant none // ==/UserScript== (() => { var to = {createHTML: s => s, createScript: s => s}, tp = window.trustedTypes?.createPolicy ? trustedTypes.createPolicy("", to) : to; var html = s => tp.createHTML(s), script = s => tp.createScript(s); var ele = document.createElement("SCRIPT"); ele.text = script("(" + (function() { var resizeUpdateDelay = 300; var eleVideo, baseWidth, baseHeight, posX, posY, deltaX, deltaY, scaleX, scaleY; var eleContainer, containerWidth, containerHeight, configs; var changing = false, timerIdChange = 0, timerIdUpdateAll = 0; var to = {createHTML: s => s, createScript: s => s}, tp = window.trustedTypes?.createPolicy ? trustedTypes.createPolicy("", to) : to; var html = s => tp.createHTML(s), script = s => tp.createScript(s); function doneChange() { changing = false; clearTimeout(timerIdChange); timerIdChange = 0; } function doChange() { changing = true; if (timerIdChange) clearTimeout(timerIdChange); timerIdChange = setTimeout(doneChange, 100); } function setPos(dx, dy) { var rw = 1, rh = 1; if (document.fullscreen) { rw = screen.width / eleContainer.offsetWidth; rh = screen.height / eleContainer.offsetHeight; } if (dx !== undefined) { deltaX += dx; deltaY += dy; } else { posX = 0; posY = 0; deltaX = 0; deltaY = 0; } doChange(); eleVideo.style.left = ((posX + deltaX) * rw) + "px"; eleVideo.style.top = ((posY + deltaY) * rh) + "px"; eleVideo.style.width = "100%"; } function setSize(dx, dy) { var rw = 1, rh = 1; if (document.fullscreen) { rw = screen.width / eleContainer.offsetWidth; rh = screen.height / eleContainer.offsetHeight; } if (dx !== undefined) { scaleX += dx; scaleY += dy; } else { scaleX = 1; scaleY = 1; } doChange(); eleVideo.style.MozTransform = eleVideo.style.WebkitTransform = "scaleX(" + (scaleX*rw).toFixed(2) + ") scaleY(" + (scaleY*rh).toFixed(2) + ")"; } function updateAll() { var rw = 1, rh = 1, px = posX + deltaX, py = posY + deltaY; if (document.fullscreen) { rw = screen.width / eleContainer.offsetWidth; rh = screen.height / eleContainer.offsetHeight; } doChange(); eleVideo.style.left = (px * rw).toFixed(0) + "px"; eleVideo.style.top = (py * rh).toFixed(0) + "px"; eleVideo.style.width = "100%"; eleVideo.style.MozTransform = eleVideo.style.WebkitTransform = "scaleX(" + (scaleX*rw).toFixed(2) + ") scaleY(" + (scaleY*rh).toFixed(2) + ")"; vpzConfigs.style.top = document.fullscreen ? "-3px" : ""; clearTimeout(timerIdUpdateAll); timerIdUpdateAll = 0; } function delayedUpdateAll() { if (timerIdUpdateAll) clearTimeout(timerIdUpdateAll); timerIdUpdateAll = setTimeout(updateAll, 100); } function setup() { var vpzPanel = window.vpzPanel; if (!vpzPanel) { vpzPanel = document.createElement("DIV"); vpzPanel.id = "vpzPanel"; vpzPanel.innerHTML = html(`<style> #vpzPanel{position:relative;float:left;margin:10px 0 0 20px;white-space:nowrap} #vpzPanel button{vertical-align:top;border:none;border-radius:3px;padding:0;width:18px;height:18px;line-height:0;font-size:15px;font-weight:bold;cursor:pointer} #vpzPanel button:hover{background:#bdb} #vpzMoveLeft{margin-left:0} #vpzMoveL,#vpzShrink,#vpzShrinkH,#vpzShrinkV,#vpzConfig{margin-left:10px!important} #vpzCfgContainer{display:none;position:absolute;z-index:99;right:0;bottom:55px;padding:5px;line-height:normal;background:#555} #vpzCfgContainer button{height:21px;padding:0 5px} #vpzConfigs{position:relative} #vpzConfigs~button{width:auto} </style> <button id="vpzReset" class="yt-uix-button-default" title="Reset">0</button> <button id="vpzMoveL" class="yt-uix-button-default" title="Move Left">←</button> <button id="vpzMoveU" class="yt-uix-button-default" title="Move Up">↑</button> <button id="vpzMoveD" class="yt-uix-button-default" title="Move Down">↓</button> <button id="vpzMoveR" class="yt-uix-button-default" title="Move Right">→</button> <button id="vpzShrink" class="yt-uix-button-default" title="Shrink">↙</button> <button id="vpzExpand" class="yt-uix-button-default" title="Expand">↗</button> <button id="vpzShrinkH" class="yt-uix-button-default" title="Shrink Horizontal">⇇</button> <button id="vpzExpandH" class="yt-uix-button-default" title="Expand Horizontal">⇉</button> <button id="vpzShrinkV" class="yt-uix-button-default" title="Shrink Vertical">⇊</button> <button id="vpzExpandV" class="yt-uix-button-default" title="Expand Vertical">⇈</button> <button id="vpzConfig" class="yt-uix-button-default" title="Show/Hide Profiles Panel">P</button> <div id="vpzCfgContainer"> Configs: <select id="vpzConfigs"></select> <button id="vpzSaveCfg" class="yt-uix-button-default">Save</button> <button id="vpzLoadCfg" class="yt-uix-button-default">Load</button> <button id="vpzDelCfg" class="yt-uix-button-default">Delete</button> </div>`); var a = window["movie_player"].querySelector(".ytp-chrome-controls .ytp-right-controls"); a.parentNode.insertBefore(vpzPanel, a); vpzReset.onclick = function() { setPos(); setSize(); }; vpzMoveL.onclick = function() { setPos(-8, 0); }; vpzMoveU.onclick = function() { setPos(0, -8); }; vpzMoveD.onclick = function() { setPos(0, 8); }; vpzMoveR.onclick = function() { setPos(8, 0); }; vpzShrink.onclick = function() { setSize(-0.01, -0.01); }; vpzExpand.onclick = function() { setSize(0.01, 0.01); }; vpzShrinkH.onclick = function() { setSize(-0.01, 0); }; vpzExpandH.onclick = function() { setSize(0.01, 0); }; vpzShrinkV.onclick = function() { setSize(0, -0.01); }; vpzExpandV.onclick = function() { setSize(0, 0.01); }; vpzConfig.onclick = function() { vpzCfgContainer.style.display = vpzCfgContainer.style.display ? "" : "block"; }; var i, opt; for (i = 0; i < configs.length; i++) { opt = document.createElement("OPTION"); opt.value = i; opt.textContent = configs[i].name; vpzConfigs.appendChild(opt); } function configIndex(cfgName) { for (var i = configs.length-1; i >= 0; i--) { if (configs[i].name === cfgName) { return i; break; } } return -1; } function optionIndex(idx) { for (var i = configs.length-1; i >= 0; i--) { if (vpz.options[i].value == idx) { return i; break; } } return -1; } vpzSaveCfg.onclick = function() { var cfgName, idx, i, opt; if (vpzConfigs.selectedIndex >= 0) { cfgName = vpzConfigs.selectedOptions[0].textContent; } else { cfgName = ""; } cfgName = prompt("Enter configuration name.", cfgName); if (cfgName === null) return; cfgName = cfgName.trim(); if (!cfgName) return; idx = configIndex(cfgName); if (idx >= 0) { if (!confirm("Replace existing configuration?")) return; vpzConfigs.options[optionIndex(idx)].textContent = cfgName; } else { idx = configs.length; opt = document.createElement("OPTION"); opt.value = idx; opt.textContent = cfgName; vpzConfigs.appendChild(opt); vpzConfigs.selectedIndex = idx; } configs.splice(idx, 1, { name: cfgName, data: [deltaX, deltaY, scaleX, scaleY] }); localStorage.vpzConfigs = JSON.stringify(configs); }; vpzLoadCfg.onclick = function() { var idx; if (vpzConfigs.selectedIndex < 0) return; idx = parseInt(vpzConfigs.selectedOptions[0].value); setPos(); setPos(configs[idx].data[0], configs[idx].data[1]); scaleX = 0; scaleY = 0; setSize(configs[idx].data[2], configs[idx].data[3]); }; vpzDelCfg.onclick = function() { if ((vpzConfigs.selectedIndex < 0) || !confirm("Delete selected configuration?")) return; configs.splice(vpzConfigs.selectedOptions[0].value, 1); localStorage.vpzConfigs = JSON.stringify(configs); vpzConfigs.removeChild(vpzConfigs.selectedOptions[0]); }; } } function init() { eleVideo = document.querySelector(".html5-main-video"); if (eleVideo) { baseWidth = eleVideo.offsetWidth; baseHeight = eleVideo.offsetHeight; posX = eleVideo.offsetLeft; posY = eleVideo.offsetTop; deltaX = 0; deltaY = 0; scaleX = 1; scaleY = 1; eleContainer = eleVideo.parentNode.parentNode; containerWidth = eleContainer.offsetWidth; containerHeight = eleContainer.offsetHeight; configs = JSON.parse(localStorage.vpzConfigs || "[]"); if (eleVideo.videoHeight) { eleVideo.style.left = "0px"; eleVideo.style.top = "0px"; eleVideo.style.width = "100%"; baseWidth = eleVideo.offsetWidth; baseHeight = eleVideo.offsetHeight; setup(); } else { setTimeout(init, 100); } (new MutationObserver(function(records, obj) { if (!changing) delayedUpdateAll(); })).observe(eleVideo, { attributes: true, attributeFilter: ["style"] }); } } init(); addEventListener("spfdone", init, false); addEventListener("resize", function() { if (!eleVideo || !window.vpzConfigs) return; setTimeout(updateAll, resizeUpdateDelay); }, false); }).toString() + ")()"); document.head.appendChild(ele); })()