动画疯截图助手

动画疯截图工具,支援快捷键截图、连拍模式,自定义快捷键、连拍间隔设定、中英菜单切换

  1. // ==UserScript==
  2. // @name AniGamer Screenshot Helper
  3. // @name:zh-TW 動畫瘋截圖助手
  4. // @name:zh-CN 动画疯截图助手
  5. // @namespace https://www.tampermonkey.net/
  6. // @version 2.0
  7. // @description AniGamer Screenshot Tool – supports hotkey capture, burst mode, customizable hotkeys, burst interval settings, and menu language switch between Chinese and English.
  8. // @description:zh-TW 動畫瘋截圖工具,支援快捷鍵截圖、連拍模式,自定義快捷鍵、連拍間隔設定、中英菜單切換
  9. // @description:zh-CN 动画疯截图工具,支援快捷键截图、连拍模式,自定义快捷键、连拍间隔设定、中英菜单切换
  10. // @author ChatGPT
  11. // @match https://ani.gamer.com.tw/*
  12. // @grant GM_registerMenuCommand
  13. // @grant GM_getValue
  14. // @grant GM_setValue
  15. // @license MIT
  16. // ==/UserScript==
  17.  
  18. (function () {
  19. 'use strict';
  20.  
  21. // 預設快捷鍵
  22. const DEFAULT_SHORTCUT = 'S';
  23. // 預設連拍間隔(毫秒)
  24. const DEFAULT_INTERVAL = 1000;
  25. // 最小連拍間隔(毫秒)
  26. const MIN_INTERVAL = 100;
  27.  
  28. // 多語系字典
  29. const i18n = {
  30. EN: {
  31. langSwitch: 'LANG EN',
  32. setKey: key => `Set Shortcut Key (Current: ${key})`,
  33. setInterval: ms => `Set Burst Interval (Current: ${ms}ms)`,
  34. inputKey: 'Enter a new shortcut key (one character):',
  35. inputInterval: 'Enter new burst interval in ms (min: 100):',
  36. invalidInterval: 'Invalid input. Must be ≥ 100.'
  37. },
  38. ZH: {
  39. langSwitch: '語言 中文',
  40. setKey: key => `設定快捷鍵(目前:${key})`,
  41. setInterval: ms => `設定連拍間隔(目前:${ms}ms)`,
  42. inputKey: '請輸入新的截圖快捷鍵(單一字母):',
  43. inputInterval: '請輸入新的連拍間隔(單位:毫秒,最小值:100):',
  44. invalidInterval: '請輸入一個不小於 100 的有效數字。'
  45. }
  46. };
  47.  
  48. // 取得目前設定(語言、快捷鍵、連拍間隔)
  49. function getSettings() {
  50. return {
  51. lang: GM_getValue('lang', 'EN'),
  52. shortcutKey: GM_getValue('screenshotKey', DEFAULT_SHORTCUT),
  53. interval: parseInt(GM_getValue('burstInterval', DEFAULT_INTERVAL), 10)
  54. };
  55. }
  56.  
  57. // 產生檔名:影片標題_小時_分鐘_秒_毫秒_解析度.png
  58. function generateFilename(video, canvas) {
  59. // 補零用
  60. const pad = (n, len = 2) => n.toString().padStart(len, '0');
  61. const time = video.currentTime;
  62. const h = pad(Math.floor(time / 3600));
  63. const m = pad(Math.floor((time % 3600) / 60));
  64. const s = pad(Math.floor(time % 60));
  65. const ms = pad(Math.floor((time % 1) * 1000), 3);
  66. // 取得網頁標題並移除非法字元
  67. const rawTitle = document.title;
  68. const cleanedTitle = rawTitle.replace(/[\s\\/:*?"<>|]/g, '_');
  69. // 解析度
  70. const resolution = `${canvas.width}x${canvas.height}`;
  71. // 修改命名規則:影片標題_小時_分鐘_秒_毫秒_解析度
  72. return `${cleanedTitle}_${h}_${m}_${s}_${ms}_${resolution}.png`;
  73. }
  74.  
  75. // 截圖主程式
  76. function captureScreenshot() {
  77. const video = document.querySelector('video');
  78. // 若找不到影片或影片未載入完成則不執行
  79. if (!video || video.readyState < 2) return;
  80.  
  81. // 建立 canvas 並繪製當前影像
  82. const canvas = document.createElement('canvas');
  83. canvas.width = video.videoWidth;
  84. canvas.height = video.videoHeight;
  85. const ctx = canvas.getContext('2d');
  86. ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
  87.  
  88. // 產生檔名
  89. const filename = generateFilename(video, canvas);
  90.  
  91. // 轉成 blob 並觸發下載
  92. canvas.toBlob(blob => {
  93. const a = document.createElement('a');
  94. a.href = URL.createObjectURL(blob);
  95. a.download = filename;
  96. a.click();
  97. // 延遲釋放 blob,避免尚未下載即釋放
  98. setTimeout(() => URL.revokeObjectURL(a.href), 1000);
  99. }, 'image/png');
  100. }
  101.  
  102. // 綁定快捷鍵與連拍事件
  103. (function bindHotkeys() {
  104. let isPressing = false; // 是否正在按壓
  105. let burstTimer = null; // 連拍計時器
  106.  
  107. window.addEventListener('keydown', e => {
  108. const { shortcutKey, interval } = getSettings();
  109. // 按下自訂快捷鍵且未重複觸發時
  110. if (e.key.toUpperCase() === shortcutKey.toUpperCase() && !isPressing) {
  111. isPressing = true;
  112. captureScreenshot();
  113. burstTimer = setInterval(captureScreenshot, interval);
  114. }
  115. });
  116.  
  117. window.addEventListener('keyup', e => {
  118. const { shortcutKey } = getSettings();
  119. // 放開自訂快捷鍵時停止連拍
  120. if (e.key.toUpperCase() === shortcutKey.toUpperCase()) {
  121. isPressing = false;
  122. if (burstTimer) clearInterval(burstTimer);
  123. }
  124. });
  125. })();
  126.  
  127. // 註冊油猴右上角選單
  128. (function registerMenu() {
  129. // 動態取得目前語系與設定
  130. function t() {
  131. const { lang, shortcutKey, interval } = getSettings();
  132. return { ...i18n[lang], shortcutKey, interval, lang };
  133. }
  134.  
  135. // 設定快捷鍵
  136. GM_registerMenuCommand(t().setKey(t().shortcutKey), () => {
  137. const input = prompt(t().inputKey, t().shortcutKey);
  138. // 僅接受單一字元
  139. if (input && input.length === 1) {
  140. GM_setValue('screenshotKey', input.toUpperCase());
  141. location.reload(); // 直接重新整理,不跳提示
  142. }
  143. });
  144.  
  145. // 設定連拍間隔
  146. GM_registerMenuCommand(t().setInterval(t().interval), () => {
  147. const input = prompt(t().inputInterval, t().interval);
  148. const newVal = parseInt(input, 10);
  149. if (!isNaN(newVal) && newVal >= MIN_INTERVAL) {
  150. GM_setValue('burstInterval', newVal);
  151. location.reload(); // 直接重新整理,不跳提示
  152. } else {
  153. alert(t().invalidInterval); // 僅錯誤時提示
  154. }
  155. });
  156.  
  157. // 語言切換
  158. GM_registerMenuCommand(t().langSwitch, () => {
  159. const newLang = t().lang === 'EN' ? 'ZH' : 'EN';
  160. GM_setValue('lang', newLang);
  161. location.reload();
  162. });
  163. })();
  164.  
  165. })();