Greasy Fork 还支持 简体中文。

Web Speed Controller

control the speed of website timers, animations, and videos without changing video playback rate

  1. // ==UserScript==
  2. // @name Web Speed Controller
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.9
  5. // @description control the speed of website timers, animations, and videos without changing video playback rate
  6. // @author Minoa
  7. // @match *://*/*
  8. // @grant none
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. // Create UI elements
  17. const controls = document.createElement('div');
  18. controls.style.cssText = `
  19. position: fixed;
  20. top: 13px;
  21. right: 18px;
  22. background: rgba(15, 23, 42, 0.8);
  23. padding: 4px;
  24. border: 1px solid rgba(255, 255, 255, 0.08);
  25. border-radius: 8px;
  26. z-index: 9999999;
  27. display: flex;
  28. gap: 4px;
  29. box-shadow: 0 4px 16px rgba(0, 0, 0, 0.22);
  30. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
  31. align-items: center;
  32. transition: all 0.3s ease;
  33. width: 45px;
  34. overflow: hidden;
  35. `;
  36.  
  37. const input = document.createElement('input');
  38. input.type = 'number';
  39. input.step = '1';
  40. input.value = '1';
  41. input.style.cssText = `
  42. width: 22px;
  43. height: 22px;
  44. background: rgba(30, 41, 59, 0.8);
  45. border: 1px solid rgba(148, 163, 184, 0.1);
  46. color: rgba(226, 232, 240, 0.6);
  47. border-radius: 6px;
  48. padding: 2px;
  49. font-size: 12px;
  50. font-weight: 500;
  51. text-align: center;
  52. outline: none;
  53. transition: all 0.3s ease;
  54. -moz-appearance: textfield;
  55. cursor: pointer;
  56. `;
  57.  
  58. // Remove spinner arrows
  59. input.style.cssText += `
  60. &::-webkit-outer-spin-button,
  61. &::-webkit-inner-spin-button {
  62. -webkit-appearance: none;
  63. margin: 0;
  64. }
  65. `;
  66.  
  67. const toggleButton = document.createElement('button');
  68. toggleButton.textContent = '▶';
  69. toggleButton.style.cssText = `
  70. background: rgba(59, 130, 246, 0.5);
  71. color: rgba(255, 255, 255, 0.6);
  72. border: none;
  73. border-radius: 6px;
  74. width: 20px;
  75. height: 20px;
  76. font-size: 10px;
  77. font-weight: 600;
  78. cursor: pointer;
  79. transition: all 0.3s ease;
  80. display: none;
  81. align-items: center;
  82. justify-content: center;
  83. text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
  84. white-space: nowrap;
  85. padding: 0;
  86. `;
  87.  
  88. let isExpanded = false;
  89. let isEnabled = false;
  90.  
  91. // Hover effect for small state
  92. controls.addEventListener('mouseenter', () => {
  93. if (!isExpanded) {
  94. controls.style.background = 'rgba(15, 23, 42, 0.45)';
  95. input.style.background = 'rgba(30, 41, 59, 0.5)';
  96. input.style.color = 'rgba(226, 232, 240, 0.8)';
  97. }
  98. });
  99.  
  100. controls.addEventListener('mouseleave', () => {
  101. if (!isExpanded) {
  102. controls.style.background = 'rgba(15, 23, 42, 0.25)';
  103. input.style.background = 'rgba(30, 41, 59, 0.3)';
  104. input.style.color = 'rgba(226, 232, 240, 0.6)';
  105. }
  106. });
  107.  
  108. function expandControls() {
  109. if (!isExpanded) {
  110. controls.style.width = 'auto';
  111. controls.style.padding = '16px';
  112. controls.style.background = 'rgba(15, 23, 42, 0.85)';
  113. controls.style.backdropFilter = 'blur(10px)';
  114. controls.style.webkitBackdropFilter = 'blur(10px)';
  115. controls.style.borderRadius = '12px';
  116. controls.style.gap = '12px';
  117. controls.style.boxShadow = '0 8px 32px rgba(0, 0, 0, 0.2)';
  118. controls.style.border = '1px solid rgba(255, 255, 255, 0.1)';
  119.  
  120. input.style.width = '70px';
  121. input.style.height = '36px';
  122. input.style.padding = '4px 8px';
  123. input.style.fontSize = '14px';
  124. input.style.background = 'rgba(30, 41, 59, 0.8)';
  125. input.style.borderRadius = '8px';
  126. input.style.border = '2px solid rgba(148, 163, 184, 0.2)';
  127. input.style.color = '#e2e8f0';
  128.  
  129. toggleButton.style.display = 'flex';
  130. toggleButton.style.width = '90px';
  131. toggleButton.style.height = '36px';
  132. toggleButton.style.padding = '8px 16px';
  133. toggleButton.style.fontSize = '14px';
  134. toggleButton.style.borderRadius = '8px';
  135. toggleButton.textContent = 'Enable';
  136. toggleButton.style.background = '#3b82f6';
  137. toggleButton.style.color = '#ffffff';
  138.  
  139. isExpanded = true;
  140. }
  141. }
  142.  
  143. function adjustInputWidth() {
  144. if (isExpanded) {
  145. const span = document.createElement('span');
  146. span.style.cssText = `
  147. position: absolute;
  148. top: -9999px;
  149. font: ${window.getComputedStyle(input).font};
  150. padding: ${window.getComputedStyle(input).padding};
  151. `;
  152. span.textContent = input.value;
  153. document.body.appendChild(span);
  154. const newWidth = Math.max(70, span.offsetWidth + 24);
  155. input.style.width = `${newWidth}px`;
  156. document.body.removeChild(span);
  157. }
  158. }
  159.  
  160. // Expand on input focus
  161. input.addEventListener('focus', () => {
  162. expandControls();
  163. input.style.borderColor = '#3b82f6';
  164. input.style.boxShadow = '0 0 0 3px rgba(59, 130, 246, 0.3)';
  165. });
  166.  
  167. input.addEventListener('blur', () => {
  168. if (isExpanded) {
  169. input.style.borderColor = 'rgba(148, 163, 184, 0.2)';
  170. input.style.boxShadow = 'none';
  171. }
  172. input.value = Math.round(input.value) || 1;
  173. });
  174.  
  175. // Handle input changes
  176. input.addEventListener('input', () => {
  177. input.value = Math.round(input.value);
  178. adjustInputWidth();
  179. });
  180.  
  181. // Keyboard navigation
  182. input.addEventListener('keydown', (e) => {
  183. const currentValue = parseInt(input.value) || 1;
  184. if (e.key === 'ArrowUp') {
  185. e.preventDefault();
  186. const newValue = currentValue + 1;
  187. input.value = newValue;
  188. adjustInputWidth();
  189. if (isEnabled) updateSpeed();
  190. } else if (e.key === 'ArrowDown') {
  191. e.preventDefault();
  192. const newValue = Math.max(1, currentValue - 1);
  193. input.value = newValue;
  194. adjustInputWidth();
  195. if (isEnabled) updateSpeed();
  196. }
  197. });
  198.  
  199. // Button styling
  200. toggleButton.addEventListener('mouseover', () => {
  201. if (isExpanded) {
  202. toggleButton.style.background = isEnabled ? '#dc2626' : '#2563eb';
  203. toggleButton.style.transform = 'translateY(-1px)';
  204. }
  205. });
  206.  
  207. toggleButton.addEventListener('mouseout', () => {
  208. if (isExpanded) {
  209. toggleButton.style.background = isEnabled ? '#ef4444' : '#3b82f6';
  210. toggleButton.style.transform = 'translateY(0)';
  211. }
  212. });
  213.  
  214. // Store original timing functions
  215. const original = {
  216. setTimeout: window.setTimeout.bind(window),
  217. setInterval: window.setInterval.bind(window),
  218. requestAnimationFrame: window.requestAnimationFrame.bind(window),
  219. dateNow: Date.now.bind(Date),
  220. originalDate: Date,
  221. // Store original media element methods
  222. mediaElementCurrentTime: Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'currentTime'),
  223. mediaElementPlaybackRate: Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'playbackRate'),
  224. mediaElementPlay: HTMLMediaElement.prototype.play,
  225. mediaElementPause: HTMLMediaElement.prototype.pause
  226. };
  227.  
  228. let speedMultiplier = 1;
  229. let startTime = original.dateNow();
  230. let modifiedTime = startTime;
  231.  
  232. function updateSpeed() {
  233. speedMultiplier = parseInt(input.value) || 1;
  234. modifiedTime = original.dateNow();
  235. startTime = original.dateNow();
  236. applySpeedMultiplier();
  237. }
  238.  
  239. function applySpeedMultiplier() {
  240. window.setTimeout = function(callback, delay, ...args) {
  241. return original.setTimeout(callback, delay / speedMultiplier, ...args);
  242. };
  243.  
  244. window.setInterval = function(callback, delay, ...args) {
  245. return original.setInterval(callback, delay / speedMultiplier, ...args);
  246. };
  247.  
  248. window.requestAnimationFrame = function(callback) {
  249. return original.requestAnimationFrame((timestamp) => {
  250. const adjustedTimestamp = timestamp * speedMultiplier;
  251. callback(adjustedTimestamp);
  252. });
  253. };
  254.  
  255. function TimeWarpDate(...args) {
  256. if (args.length === 0) {
  257. const now = original.dateNow();
  258. const timePassed = now - startTime;
  259. const adjustedTime = modifiedTime + (timePassed * speedMultiplier);
  260. return new original.originalDate(adjustedTime);
  261. }
  262. return new original.originalDate(...args);
  263. }
  264.  
  265. TimeWarpDate.prototype = original.originalDate.prototype;
  266.  
  267. TimeWarpDate.now = function() {
  268. const now = original.dateNow();
  269. const timePassed = now - startTime;
  270. return modifiedTime + (timePassed * speedMultiplier);
  271. };
  272.  
  273. TimeWarpDate.parse = original.originalDate.parse;
  274. TimeWarpDate.UTC = original.originalDate.UTC;
  275.  
  276. window.Date = TimeWarpDate;
  277. // Override HTMLMediaElement methods to control video timing
  278. const mediaElements = {};
  279. const mediaStartTimes = {};
  280. // Override currentTime getter and setter
  281. Object.defineProperty(HTMLMediaElement.prototype, 'currentTime', {
  282. get: function() {
  283. const originalGet = original.mediaElementCurrentTime.get;
  284. const actualTime = originalGet.call(this);
  285. return actualTime;
  286. },
  287. set: function(value) {
  288. const originalSet = original.mediaElementCurrentTime.set;
  289. const id = this.dataset.speedControllerId || (this.dataset.speedControllerId = Math.random().toString(36).substr(2, 9));
  290. mediaElements[id] = this;
  291. // Store the start time if not already stored
  292. if (!mediaStartTimes[id]) {
  293. mediaStartTimes[id] = {
  294. realTime: original.dateNow(),
  295. mediaTime: value
  296. };
  297. }
  298. originalSet.call(this, value);
  299. },
  300. configurable: true
  301. });
  302. // Override play method
  303. HTMLMediaElement.prototype.play = function() {
  304. const id = this.dataset.speedControllerId || (this.dataset.speedControllerId = Math.random().toString(36).substr(2, 9));
  305. mediaElements[id] = this;
  306. // Update start time when play is called
  307. mediaStartTimes[id] = {
  308. realTime: original.dateNow(),
  309. mediaTime: this.currentTime
  310. };
  311. return original.mediaElementPlay.call(this);
  312. };
  313. // Override pause method
  314. HTMLMediaElement.prototype.pause = function() {
  315. return original.mediaElementPause.call(this);
  316. };
  317. // Create a function to update all media elements
  318. function updateMediaElements() {
  319. for (const id in mediaElements) {
  320. const element = mediaElements[id];
  321. if (!element.paused && mediaStartTimes[id]) {
  322. const originalGet = original.mediaElementCurrentTime.get;
  323. const originalSet = original.mediaElementCurrentTime.set;
  324. const currentRealTime = original.dateNow();
  325. const realTimePassed = currentRealTime - mediaStartTimes[id].realTime;
  326. const adjustedTimePassed = realTimePassed * speedMultiplier;
  327. const newMediaTime = mediaStartTimes[id].mediaTime + (adjustedTimePassed / 1000);
  328. // Only update if the difference is significant
  329. const currentMediaTime = originalGet.call(element);
  330. if (Math.abs(newMediaTime - currentMediaTime) > 0.1) {
  331. originalSet.call(element, newMediaTime);
  332. // Update start time to prevent drift
  333. mediaStartTimes[id] = {
  334. realTime: currentRealTime,
  335. mediaTime: newMediaTime
  336. };
  337. }
  338. }
  339. }
  340. // Continue updating media elements
  341. if (speedMultiplier !== 1) {
  342. original.setTimeout(updateMediaElements, 100);
  343. }
  344. }
  345. // Start updating media elements
  346. if (speedMultiplier !== 1) {
  347. updateMediaElements();
  348. }
  349. }
  350.  
  351. function restoreOriginal() {
  352. window.setTimeout = original.setTimeout;
  353. window.setInterval = original.setInterval;
  354. window.requestAnimationFrame = original.requestAnimationFrame;
  355. window.Date = original.originalDate;
  356. modifiedTime = original.dateNow();
  357. startTime = original.dateNow();
  358. // Restore HTMLMediaElement methods
  359. Object.defineProperty(HTMLMediaElement.prototype, 'currentTime', original.mediaElementCurrentTime);
  360. HTMLMediaElement.prototype.play = original.mediaElementPlay;
  361. HTMLMediaElement.prototype.pause = original.mediaElementPause;
  362. }
  363.  
  364. input.addEventListener('change', () => {
  365. if (isEnabled) {
  366. updateSpeed();
  367. }
  368. });
  369.  
  370. toggleButton.addEventListener('click', () => {
  371. isEnabled = !isEnabled;
  372. toggleButton.textContent = isEnabled ? 'Disable' : 'Enable';
  373. toggleButton.style.background = isEnabled ? '#ef4444' : '#3b82f6';
  374.  
  375. if (isEnabled) {
  376. updateSpeed();
  377. } else {
  378. restoreOriginal();
  379. }
  380. });
  381.  
  382. controls.appendChild(input);
  383. controls.appendChild(toggleButton);
  384. document.body.appendChild(controls);
  385. })();