Greasy Fork 还支持 简体中文。

YouTube HTML5 Video Pan And Zoom

Add controls to pan and zoom HTML5 video.

目前為 2024-08-12 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name YouTube HTML5 Video Pan And Zoom
  3. // @namespace YouTubeHTML5VideoPanAndZoom
  4. // @description Add controls to pan and zoom HTML5 video.
  5. // @author jcunews
  6. // @include https://www.youtube.com/watch*
  7. // @version 1.1.8
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (() => {
  12. var to = {createHTML: s => s, createScript: s => s}, tp = window.trustedTypes?.createPolicy ? trustedTypes.createPolicy("", to) : to;
  13. var html = s => tp.createHTML(s), script = s => tp.createScript(s);
  14. var ele = document.createElement("SCRIPT");
  15. ele.text = script("(" + (function() {
  16. var resizeUpdateDelay = 300;
  17. var eleVideo, baseWidth, baseHeight, posX, posY, deltaX, deltaY, scaleX, scaleY;
  18. var eleContainer, containerWidth, containerHeight, configs;
  19. var changing = false, timerIdChange = 0, timerIdUpdateAll = 0;
  20. var to = {createHTML: s => s, createScript: s => s}, tp = window.trustedTypes?.createPolicy ? trustedTypes.createPolicy("", to) : to;
  21. var html = s => tp.createHTML(s), script = s => tp.createScript(s);
  22.  
  23. function doneChange() {
  24. changing = false;
  25. clearTimeout(timerIdChange);
  26. timerIdChange = 0;
  27. }
  28.  
  29. function doChange() {
  30. changing = true;
  31. if (timerIdChange) clearTimeout(timerIdChange);
  32. timerIdChange = setTimeout(doneChange, 100);
  33. }
  34.  
  35. function setPos(dx, dy) {
  36. var rw = 1, rh = 1;
  37. if (fullScreen) {
  38. rw = screen.width / eleContainer.offsetWidth;
  39. rh = screen.height / eleContainer.offsetHeight;
  40. }
  41. if (dx !== undefined) {
  42. deltaX += dx;
  43. deltaY += dy;
  44. } else {
  45. posX = 0;
  46. posY = 0;
  47. deltaX = 0;
  48. deltaY = 0;
  49. }
  50. doChange();
  51. eleVideo.style.left = ((posX + deltaX) * rw) + "px";
  52. eleVideo.style.top = ((posY + deltaY) * rh) + "px";
  53. eleVideo.style.width = "100%";
  54. }
  55.  
  56. function setSize(dx, dy) {
  57. var rw = 1, rh = 1;
  58. if (fullScreen) {
  59. rw = screen.width / eleContainer.offsetWidth;
  60. rh = screen.height / eleContainer.offsetHeight;
  61. }
  62. if (dx !== undefined) {
  63. scaleX += dx;
  64. scaleY += dy;
  65. } else {
  66. scaleX = 1;
  67. scaleY = 1;
  68. }
  69. doChange();
  70. eleVideo.style.MozTransform = eleVideo.style.WebkitTransform =
  71. "scaleX(" + (scaleX*rw).toFixed(2) + ") scaleY(" + (scaleY*rh).toFixed(2) + ")";
  72. }
  73.  
  74. function updateAll() {
  75. var rw = 1, rh = 1, px = posX + deltaX, py = posY + deltaY;
  76. if (fullScreen) {
  77. rw = screen.width / eleContainer.offsetWidth;
  78. rh = screen.height / eleContainer.offsetHeight;
  79. }
  80. doChange();
  81. eleVideo.style.left = (px * rw).toFixed(0) + "px";
  82. eleVideo.style.top = (py * rh).toFixed(0) + "px";
  83. eleVideo.style.width = "100%";
  84. eleVideo.style.MozTransform = eleVideo.style.WebkitTransform =
  85. "scaleX(" + (scaleX*rw).toFixed(2) + ") scaleY(" + (scaleY*rh).toFixed(2) + ")";
  86. vpzConfigs.style.top = fullScreen ? "-3px" : "";
  87. clearTimeout(timerIdUpdateAll);
  88. timerIdUpdateAll = 0;
  89. }
  90.  
  91. function delayedUpdateAll() {
  92. if (timerIdUpdateAll) clearTimeout(timerIdUpdateAll);
  93. timerIdUpdateAll = setTimeout(updateAll, 100);
  94. }
  95.  
  96. function setup() {
  97. var vpzPanel = window.vpzPanel;
  98. if (!vpzPanel) {
  99. vpzPanel = document.createElement("DIV");
  100. vpzPanel.id = "vpzPanel";
  101. vpzPanel.innerHTML = html(`<style>
  102. #vpzPanel{position:relative;float:left;margin:10px 0 0 20px;white-space:nowrap}
  103. #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}
  104. #vpzPanel button:hover{background:#bdb}
  105. #vpzMoveLeft{margin-left:0}
  106. #vpzMoveL,#vpzShrink,#vpzShrinkH,#vpzShrinkV,#vpzConfig{margin-left:10px!important}
  107. #vpzCfgContainer{display:none;position:absolute;z-index:99;right:0;bottom:55px;padding:5px;line-height:normal;background:#555}
  108. #vpzCfgContainer button{height:21px;padding:0 5px}
  109. #vpzConfigs{position:relative}
  110. #vpzConfigs~button{width:auto}
  111. </style>
  112. <button id="vpzReset" class="yt-uix-button-default" title="Reset">0</button>
  113. <button id="vpzMoveL" class="yt-uix-button-default" title="Move Left">&#x2190;</button>
  114. <button id="vpzMoveU" class="yt-uix-button-default" title="Move Up">&#x2191;</button>
  115. <button id="vpzMoveD" class="yt-uix-button-default" title="Move Down">&#x2193;</button>
  116. <button id="vpzMoveR" class="yt-uix-button-default" title="Move Right">&#x2192;</button>
  117. <button id="vpzShrink" class="yt-uix-button-default" title="Shrink">&#x2199;</button>
  118. <button id="vpzExpand" class="yt-uix-button-default" title="Expand">&#x2197;</button>
  119. <button id="vpzShrinkH" class="yt-uix-button-default" title="Shrink Horizontal">&#x21C7;</button>
  120. <button id="vpzExpandH" class="yt-uix-button-default" title="Expand Horizontal">&#x21C9;</button>
  121. <button id="vpzShrinkV" class="yt-uix-button-default" title="Shrink Vertical">&#x21CA;</button>
  122. <button id="vpzExpandV" class="yt-uix-button-default" title="Expand Vertical">&#x21C8;</button>
  123. <button id="vpzConfig" class="yt-uix-button-default" title="Show/Hide Profiles Panel">P</button>
  124. <div id="vpzCfgContainer">
  125. Configs: <select id="vpzConfigs"></select>
  126. <button id="vpzSaveCfg" class="yt-uix-button-default">Save</button>
  127. <button id="vpzLoadCfg" class="yt-uix-button-default">Load</button>
  128. <button id="vpzDelCfg" class="yt-uix-button-default">Delete</button>
  129. </div>`);
  130. var a = window["movie_player"].querySelector(".ytp-chrome-controls .ytp-right-controls");
  131. a.parentNode.insertBefore(vpzPanel, a);
  132.  
  133. vpzReset.onclick = function() {
  134. setPos();
  135. setSize();
  136. };
  137.  
  138. vpzMoveL.onclick = function() {
  139. setPos(-8, 0);
  140. };
  141. vpzMoveU.onclick = function() {
  142. setPos(0, -8);
  143. };
  144. vpzMoveD.onclick = function() {
  145. setPos(0, 8);
  146. };
  147. vpzMoveR.onclick = function() {
  148. setPos(8, 0);
  149. };
  150.  
  151. vpzShrink.onclick = function() {
  152. setSize(-0.01, -0.01);
  153. };
  154. vpzExpand.onclick = function() {
  155. setSize(0.01, 0.01);
  156. };
  157.  
  158. vpzShrinkH.onclick = function() {
  159. setSize(-0.01, 0);
  160. };
  161. vpzExpandH.onclick = function() {
  162. setSize(0.01, 0);
  163. };
  164.  
  165. vpzShrinkV.onclick = function() {
  166. setSize(0, -0.01);
  167. };
  168. vpzExpandV.onclick = function() {
  169. setSize(0, 0.01);
  170. };
  171.  
  172. vpzConfig.onclick = function() {
  173. vpzCfgContainer.style.display = vpzCfgContainer.style.display ? "" : "block";
  174. };
  175.  
  176. var i, opt;
  177. for (i = 0; i < configs.length; i++) {
  178. opt = document.createElement("OPTION");
  179. opt.value = i;
  180. opt.textContent = configs[i].name;
  181. vpzConfigs.appendChild(opt);
  182. }
  183.  
  184. function configIndex(cfgName) {
  185. for (var i = configs.length-1; i >= 0; i--) {
  186. if (configs[i].name === cfgName) {
  187. return i;
  188. break;
  189. }
  190. }
  191. return -1;
  192. }
  193. function optionIndex(idx) {
  194. for (var i = configs.length-1; i >= 0; i--) {
  195. if (vpz.options[i].value == idx) {
  196. return i;
  197. break;
  198. }
  199. }
  200. return -1;
  201. }
  202.  
  203. vpzSaveCfg.onclick = function() {
  204. var cfgName, idx, i, opt;
  205. if (vpzConfigs.selectedIndex >= 0) {
  206. cfgName = vpzConfigs.selectedOptions[0].textContent;
  207. } else {
  208. cfgName = "";
  209. }
  210. cfgName = prompt("Enter configuration name.", cfgName);
  211. if (cfgName === null) return;
  212. cfgName = cfgName.trim();
  213. if (!cfgName) return;
  214. idx = configIndex(cfgName);
  215. if (idx >= 0) {
  216. if (!confirm("Replace existing configuration?")) return;
  217. vpzConfigs.options[optionIndex(idx)].textContent = cfgName;
  218. } else {
  219. idx = configs.length;
  220. opt = document.createElement("OPTION");
  221. opt.value = idx;
  222. opt.textContent = cfgName;
  223. vpzConfigs.appendChild(opt);
  224. vpzConfigs.selectedIndex = idx;
  225. }
  226. configs.splice(idx, 1, {
  227. name: cfgName,
  228. data: [deltaX, deltaY, scaleX, scaleY]
  229. });
  230. localStorage.vpzConfigs = JSON.stringify(configs);
  231. };
  232. vpzLoadCfg.onclick = function() {
  233. var idx;
  234. if (vpzConfigs.selectedIndex < 0) return;
  235. idx = parseInt(vpzConfigs.selectedOptions[0].value);
  236. setPos();
  237. setPos(configs[idx].data[0], configs[idx].data[1]);
  238. scaleX = 0; scaleY = 0;
  239. setSize(configs[idx].data[2], configs[idx].data[3]);
  240. };
  241. vpzDelCfg.onclick = function() {
  242. if ((vpzConfigs.selectedIndex < 0) || !confirm("Delete selected configuration?")) return;
  243. configs.splice(vpzConfigs.selectedOptions[0].value, 1);
  244. localStorage.vpzConfigs = JSON.stringify(configs);
  245. vpzConfigs.removeChild(vpzConfigs.selectedOptions[0]);
  246. };
  247.  
  248. }
  249. }
  250.  
  251. function init() {
  252. eleVideo = document.querySelector(".html5-main-video");
  253. if (eleVideo) {
  254. baseWidth = eleVideo.offsetWidth;
  255. baseHeight = eleVideo.offsetHeight;
  256. posX = eleVideo.offsetLeft;
  257. posY = eleVideo.offsetTop;
  258. deltaX = 0;
  259. deltaY = 0;
  260. scaleX = 1;
  261. scaleY = 1;
  262. eleContainer = eleVideo.parentNode.parentNode;
  263. containerWidth = eleContainer.offsetWidth;
  264. containerHeight = eleContainer.offsetHeight;
  265. configs = JSON.parse(localStorage.vpzConfigs || "[]");
  266. if (eleVideo.videoHeight) {
  267. eleVideo.style.left = "0px";
  268. eleVideo.style.top = "0px";
  269. eleVideo.style.width = "100%";
  270. baseWidth = eleVideo.offsetWidth;
  271. baseHeight = eleVideo.offsetHeight;
  272. setup();
  273. } else {
  274. setTimeout(init, 100);
  275. }
  276. (new MutationObserver(function(records, obj) {
  277. if (!changing) delayedUpdateAll();
  278. })).observe(eleVideo, {
  279. attributes: true,
  280. attributeFilter: ["style"]
  281. });
  282. }
  283. }
  284. init();
  285. addEventListener("spfdone", init, false);
  286.  
  287. addEventListener("resize", function() {
  288. if (!eleVideo || !window.vpzConfigs) return;
  289. setTimeout(updateAll, resizeUpdateDelay);
  290. }, false);
  291. }).toString() + ")()");
  292. document.head.appendChild(ele);
  293. })()