├── .gitignore ├── LICENSE ├── README.md ├── background.js ├── images ├── icon desing.svg ├── icon128.png ├── icon16.png └── icon48.png ├── manifest.json ├── package-lock.json ├── package.json ├── popup ├── css │ └── style.css ├── editor.html ├── help.html ├── icons │ ├── bottomImg.png │ ├── customOffset.png │ ├── deleteImg.png │ ├── downImg.png │ ├── topImg.png │ └── upImg.png └── popup.html ├── readmeImgs ├── step1-1.png ├── step1-2.png ├── step2-1.png ├── step2-10.png ├── step2-2.png ├── step2-3.png ├── step2-4.png ├── step2-5.png ├── step2-6.png ├── step2-7.png ├── step2-8.png ├── step2-9.png └── step3-1.png └── scripts ├── editor.js ├── html2canvas.js └── popup.js /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 rlongdragon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

字幕拼貼器

2 | 3 | 這個 Chrome Extension 可以讓您快速將 YouTube 影片片段和字幕拼貼成圖片。您可以輕鬆製作梗圖、分享精彩片段。它提供一鍵截取,讓您輕鬆製作完美的 YouTube 字幕截圖。 4 | 5 | # 使用說明 6 | 7 | ## 啟動擴充功能 8 | 9 | 你可以在影片網站(YouTube、巴哈姆特動畫瘋)中開啟這個擴充功能 10 | 11 | ![](readmeImgs/step1-1.png) 12 | 13 | 你可以看到兩個按鈕 `擷取關鍵影格`以及`開啟編輯器`,開始使用時請先按下`開啟編輯器`開啟如下圖的彈出視窗編輯器。 14 | ![](readmeImgs/step1-2.png) 15 | 16 | 當你找到想記錄的關鍵幀按下`擷取關鍵幀`即可將該畫面加入至編輯器。 17 | 18 | > 你也可以使用 預設`Alt`(mac 為 `Option`) + `S` 快速擷取關鍵影格 19 | 20 | ## 使用編輯器 21 | 22 | 當你選好圖片之後,可以用上方滑桿調整字幕與畫面下緣距離,滑桿左右兩輸入框是滑桿的最小以及最大值,可以調整兩數值來調正滑桿的精準度。20-60就足夠應付大部分的使用場景了。 23 | 24 | ### 右鍵選單 25 | 26 | 如果你想對特定圖片進行操作(移動圖片層級、單獨設定偏移、刪除圖片等)可以對目標圖片按下右鍵。 27 | 28 | 此時會彈出右鍵選單,而被選中的圖片則會有藍色的外框。此時你可以使用右鍵選單來對圖片進行操作。 29 | 30 | > 下面使用[人生魯宅x尊-第2頻道 - YouTube](https://www.youtube.com/@nerdzun) [【尊】我找了小玉一起來看小玉梗圖...【第二頻道】 - YouTube](https://youtu.be/Tkf8_8_nl68?si=aDExffHY4LtQBAHa) 實際應用 31 | 32 | #### 實際範例 33 | 34 | 首先,我已經捷好了一些圖片,並且已經調整好了字幕與畫面下緣距離。 35 | 36 | 我發現我第一張圖片不小心截到兩張了,我想要將第二張圖片刪除。 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | 45 | 後來我發現,影片中是先講「我覺得是時候」才講「跟你們一並告知了」。 46 | 47 | 所以我需要將兩張圖片交換位置。 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 | 56 | 然後我發現影片中有些字幕大小比較大,那些圖片需要下移一點。 57 | 58 | 59 | 60 | 61 | 62 | 63 |
64 | 65 | 下面還有幾張圖片也是要調整,這時我可以直接使用`沿用上次設定值`直接套用 66 | 67 | 68 | 69 | 70 | 71 | 72 |
73 | 74 | 最後,最後一張圖片是需要完整顯示出來的,所以我使用`露出整張圖片`來調整。 75 | 76 | 77 | 78 | 79 | 80 | 81 |
82 | 83 | ### 輸出以及儲存圖片 84 | 85 | 調整好後可以在輸出區按`產生圖片`,來渲染拼貼圖,並預覽。 86 | 87 | 88 | 89 | 渲染完後你可以直接使用`複製圖片`按鈕,快速將圖片複製到剪貼簿,或是使用`下載圖片`按鈕將圖片下載至本機。 90 | 91 | # 未來更新 92 | 93 | - 特殊排版 94 | 95 | - 兼容CC字幕 96 | 97 | - 偵測CC字幕自動生成 98 | 99 | - 生成 gif 100 | 101 | - 裁切字幕底部區域 102 | 103 | # Bug 回報、意見回饋與聯繫 104 | 105 | 本專案開發中,遇到Bug、意見回饋可不吝嗇開issus給我 :D 106 | 107 | 或是你可以使用 [discord](https://discordapp.com/users/601819508943880193) 或是 [Email](mailto:jz744335@gmail.com) 聯絡我 108 | 109 | 如果你覺得這個專案對你有幫助,歡迎給我一顆星星,這可以給我有很大的動力持續更新。 110 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | function getVideoScreenshot() { 2 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 3 | const tab = tabs[0]; 4 | 5 | function getVideoScreenshot() { 6 | v = document.querySelector('video'); 7 | c = document.createElement('canvas'); 8 | c.height = v.videoHeight || parseInt(v.style.height); 9 | c.width = v.videoWidth || parseInt(v.style.width); 10 | ctx = c.getContext('2d'); 11 | ctx.drawImage(v, 0, 0); 12 | c.toDataURL() 13 | 14 | let option = { 15 | action: "getVideoData", 16 | videoData: c.toDataURL() 17 | }; 18 | 19 | chrome.runtime.sendMessage(option) 20 | }; 21 | 22 | chrome.scripting.executeScript({ 23 | target: { tabId: tab.id }, 24 | func: getVideoScreenshot, 25 | 26 | }).then(() => console.log('擷取關鍵影格')); 27 | }); 28 | } 29 | 30 | 31 | chrome.commands.onCommand.addListener((command) => { 32 | console.log(`Command: ${command}`); 33 | switch (command) { 34 | case "screenshot": 35 | getVideoScreenshot(); 36 | break; 37 | } 38 | }); 39 | 40 | chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => { 41 | switch (request.action) { 42 | case "getVideoData": 43 | // check editor opened 44 | let isOpen = await (new Promise(async (resolve, reject) => { 45 | setTimeout(() => { 46 | resolve(0) 47 | }, 1000) 48 | 49 | let getRes = await chrome.runtime.sendMessage({ action: "checkEditorOpened" }) 50 | if (getRes) { 51 | resolve(1) 52 | } 53 | })) 54 | 55 | if (isOpen) { 56 | break 57 | } 58 | // open window 59 | chrome.windows.create({ 60 | url: chrome.runtime.getURL("./popup/editor.html"), 61 | type: "popup", 62 | width: 898, 63 | height: 400, 64 | focused: true, 65 | }); 66 | 67 | let waitForWindowLoad = await (new Promise(async (resolve, reject) => { 68 | setTimeout(() => { 69 | reject(0) 70 | }, 5000) 71 | chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => { 72 | if (request.action == "editorWindowOnload") { 73 | resolve(1) 74 | } 75 | }) 76 | })) 77 | 78 | if (waitForWindowLoad) { 79 | chrome.runtime.sendMessage(request) 80 | } 81 | 82 | break; 83 | } 84 | 85 | }); -------------------------------------------------------------------------------- /images/icon desing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 627 | -------------------------------------------------------------------------------- /images/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/images/icon128.png -------------------------------------------------------------------------------- /images/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/images/icon16.png -------------------------------------------------------------------------------- /images/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/images/icon48.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "字幕拼貼器", 4 | "version": "1.1.7", 5 | "description": "這個 Chrome Extension 可以讓您快速將 YouTube 影片片段和字幕拼貼成圖片。您可以輕鬆製作梗圖、分享精彩片段。它提供一鍵截取,讓您輕鬆製作完美的 YouTube 字幕截圖。", 6 | "icons": { 7 | "128": "images/icon128.png", 8 | "48": "images/icon48.png", 9 | "16": "images/icon16.png" 10 | }, 11 | "action": { 12 | "default_icon": "images/icon16.png", 13 | "default_popup": "popup/popup.html" 14 | }, 15 | "background": { 16 | "service_worker": "background.js", 17 | "type": "module" 18 | }, 19 | "commands": { 20 | "screenshot": { 21 | "suggested_key": { 22 | "default": "Alt+S" 23 | }, 24 | "description": "擷取關鍵影格的快捷鍵" 25 | } 26 | }, 27 | "host_permissions": [ 28 | "https://youtube.com/*" 29 | ], 30 | "permissions": [ 31 | "activeTab", 32 | "scripting" 33 | ] 34 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Captions_Stack_Generator", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "chrome-types": "^0.1.270" 9 | } 10 | }, 11 | "node_modules/chrome-types": { 12 | "version": "0.1.270", 13 | "resolved": "https://registry.npmjs.org/chrome-types/-/chrome-types-0.1.270.tgz", 14 | "integrity": "sha512-N9NUF01Nz2+5WjRJsUKrz7BOo9rLDBbu6FOgyBH7uj20XAI751JxLzP8/dF6RWgUcSYj4+h3R1/6CVGTt7d2Ug==" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "chrome-types": "^0.1.270" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /popup/css/style.css: -------------------------------------------------------------------------------- 1 | main { 2 | padding: 15px 3 | } 4 | 5 | .button-block { 6 | background-color: #4CAF50; 7 | color: white; 8 | border-radius: 5px; 9 | padding: 14px 20px; 10 | margin: 8px 0; 11 | border: none; 12 | cursor: pointer; 13 | width: 100%; 14 | font-size: 16px; 15 | transition: background-color 0.3s; 16 | } 17 | 18 | .button-block:hover { 19 | background-color: hsl(122, 39%, 36%); 20 | } 21 | 22 | .button-inline { 23 | background-color: #4CAF50; 24 | color: white; 25 | border-radius: 5px; 26 | padding: 7px 10px; 27 | border: none; 28 | cursor: pointer; 29 | font-size: 12px; 30 | transition: background-color 0.3s; 31 | vertical-align: middle; 32 | } 33 | 34 | .button-inline:hover { 35 | background-color: hsl(122, 39%, 36%); 36 | } 37 | 38 | .copyright { 39 | font-size: 12px; 40 | color: #999; 41 | margin-top: 10px; 42 | text-align: center 43 | } 44 | 45 | .container { 46 | width: 384px; 47 | display: inline-block; 48 | margin: 8px; 49 | border: 5px solid hsl(122, 39%, 49%); 50 | border-radius: 15px; 51 | padding: 8px; 52 | vertical-align: top; 53 | min-height: 100px; 54 | } 55 | 56 | .container h1 { 57 | margin: 0; 58 | } 59 | 60 | .number-ipt { 61 | width: 40px; 62 | border-radius: 5px; 63 | padding: 5px; 64 | border: hsl(122, 39%, 49%) 3px solid; 65 | vertical-align: middle; 66 | } 67 | 68 | .number-ipt :focus { 69 | border: hsl(122, 39%, 36%) 3px solid; 70 | } 71 | 72 | .custom-scrollbar::-webkit-scrollbar-track { 73 | background-color: #ffffff00; 74 | } 75 | 76 | .custom-scrollbar::-webkit-scrollbar { 77 | width: 10px; 78 | background-color: hsla(0, 0%, 79%, 0.25); 79 | } 80 | 81 | .custom-scrollbar::-webkit-scrollbar-thumb { 82 | background-color: #c9c9c9; 83 | border-radius: 5px; 84 | } 85 | 86 | .menu { 87 | position: fixed; 88 | display: none; 89 | background-color: #f0f0f0b0; 90 | border-radius: 5px; 91 | padding: 10px; 92 | z-index: 10000; 93 | backdrop-filter: blur(5px); 94 | 95 | flex-direction: column; 96 | } 97 | 98 | #menu button { 99 | background-color: #00000000; 100 | border: none; 101 | cursor: pointer; 102 | padding: 3px 6px; 103 | margin: 2px 0; 104 | padding-left: 0; 105 | width: 100%; 106 | text-align: left; 107 | } 108 | 109 | #menu button:hover { 110 | background-color: #ffffff11; 111 | outline: #4CAF50 2px solid; 112 | border-radius: 3px; 113 | } 114 | 115 | #menu button:focus { 116 | outline: #4CAF50 2px solid; 117 | border-radius: 3px; 118 | } 119 | #menu button:active { 120 | background-color: hsla(0, 0%, 0%, 0.068); 121 | } 122 | 123 | #menu button img { 124 | width: 25px; 125 | vertical-align: middle; 126 | margin-right: 5px; 127 | } 128 | 129 | 130 | #setCustomOffset h2 { 131 | margin: 0; 132 | } 133 | #setCustomOffset input { 134 | background-color: #00000000; 135 | } -------------------------------------------------------------------------------- /popup/editor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 字幕拼貼器 編輯器 10 | 23 | 24 | 25 | 26 | 27 | 28 |

YouTube 字幕拼貼器 V 1.1.7

29 |
30 |

編輯區

31 |
32 | 33 | 34 | 35 |
36 |
37 |
38 |
39 |
40 |

輸出區

41 | 圖片畫質倍率 42 | 43 | 44 | 45 |
46 |
47 |
48 |
49 | 57 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /popup/help.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 使用指引 9 | 10 | 11 | 12 | 13 | 14 | 15 |

使用指引

16 | 17 |

這個 Chrome Extension 可以讓您快速將 YouTube 影片片段和字幕拼貼成圖片。您可以輕鬆製作梗圖、分享精彩片段。它提供一鍵截取,讓您輕鬆製作完美的 YouTube 字幕截圖。

18 |

使用說明

19 |

啟動擴充功能

20 |

你可以在影片網站(YouTube、巴哈姆特動畫瘋)中開啟這個擴充功能

21 |

你可以看到兩個按鈕 擷取關鍵影格以及開啟編輯器,開始使用時請先按下開啟編輯器開啟如下圖的彈出視窗編輯器。 22 |

當你找到想記錄的關鍵幀按下擷取關鍵幀即可將該畫面加入至編輯器。

23 |
24 |

你也可以使用 預設Alt(mac 為 Option) + S 快速擷取關鍵影格

25 |
26 |

使用編輯器

27 |

當你選好圖片之後,可以用上方滑桿調整字幕與畫面下緣距離,滑桿左右兩輸入框是滑桿的最小以及最大值,可以調整兩數值來調正滑桿的精準度。20-60就足夠應付大部分的使用場景了。

28 |

右鍵選單

29 |

如果你想對特定圖片進行操作(移動圖片層級、單獨設定偏移、刪除圖片等)可以對目標圖片按下右鍵。

30 |

此時會彈出右鍵選單,而被選中的圖片則會有藍色的外框。此時你可以使用右鍵選單來對圖片進行操作。

31 |
32 |

下面使用人生魯宅x尊-第2頻道 - YouTube 【尊】我找了小玉一起來看小玉梗圖...【第二頻道】 - YouTube 實際應用

33 |
34 |

實際範例

35 |

首先,我已經捷好了一些圖片,並且已經調整好了字幕與畫面下緣距離。

36 |

我發現我第一張圖片不小心截到兩張了,我想要將第二張圖片刪除。

37 | 38 | 39 | 40 | 41 | 42 |
43 | 44 |

後來我發現,影片中是先講「我覺得是時候」才講「跟你們一並告知了」。

45 |

所以我需要將兩張圖片交換位置。

46 | 47 | 48 | 49 | 50 | 51 |
52 | 53 |

然後我發現影片中有些字幕大小比較大,那些圖片需要下移一點。

54 | 55 | 56 | 57 | 58 | 59 |
60 | 61 |

下面還有幾張圖片也是要調整,這時我可以直接使用沿用上次設定值直接套用

62 | 63 | 64 | 65 | 66 | 67 |
68 | 69 |

最後,最後一張圖片是需要完整顯示出來的,所以我使用露出整張圖片來調整。

70 | 71 | 72 | 73 | 74 | 75 |
76 | 77 |

輸出以及儲存圖片

78 |

調整好後可以在輸出區按產生圖片,來渲染拼貼圖,並預覽。

79 |

渲染完後你可以直接使用複製圖片按鈕,快速將圖片複製到剪貼簿,或是使用下載圖片按鈕將圖片下載至本機。

80 |

未來更新

81 | 89 |

Bug 回報、意見回饋與聯繫

90 |

本專案開發中,遇到Bug、意見回饋可不吝嗇開issus給我 :D

91 |

或是你可以使用 discord 或是 Email 聯絡我

92 |

如果你覺得這個專案對你有幫助,歡迎給我一顆星星,這可以給我有很大的動力持續更新。

93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /popup/icons/bottomImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/popup/icons/bottomImg.png -------------------------------------------------------------------------------- /popup/icons/customOffset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/popup/icons/customOffset.png -------------------------------------------------------------------------------- /popup/icons/deleteImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/popup/icons/deleteImg.png -------------------------------------------------------------------------------- /popup/icons/downImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/popup/icons/downImg.png -------------------------------------------------------------------------------- /popup/icons/topImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/popup/icons/topImg.png -------------------------------------------------------------------------------- /popup/icons/upImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/popup/icons/upImg.png -------------------------------------------------------------------------------- /popup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello world 6 | 7 | 8 | 9 | 10 | 11 |

YouTube 字幕拼貼器 14 | 15 |

16 | 17 | 18 |
19 |
20 | screenshot 21 |

怎麼使用?

22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /readmeImgs/step1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/readmeImgs/step1-1.png -------------------------------------------------------------------------------- /readmeImgs/step1-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/readmeImgs/step1-2.png -------------------------------------------------------------------------------- /readmeImgs/step2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/readmeImgs/step2-1.png -------------------------------------------------------------------------------- /readmeImgs/step2-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/readmeImgs/step2-10.png -------------------------------------------------------------------------------- /readmeImgs/step2-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/readmeImgs/step2-2.png -------------------------------------------------------------------------------- /readmeImgs/step2-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/readmeImgs/step2-3.png -------------------------------------------------------------------------------- /readmeImgs/step2-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/readmeImgs/step2-4.png -------------------------------------------------------------------------------- /readmeImgs/step2-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/readmeImgs/step2-5.png -------------------------------------------------------------------------------- /readmeImgs/step2-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/readmeImgs/step2-6.png -------------------------------------------------------------------------------- /readmeImgs/step2-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/readmeImgs/step2-7.png -------------------------------------------------------------------------------- /readmeImgs/step2-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/readmeImgs/step2-8.png -------------------------------------------------------------------------------- /readmeImgs/step2-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/readmeImgs/step2-9.png -------------------------------------------------------------------------------- /readmeImgs/step3-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlongdragon/Captions_Stack_Generator/8f39f7d60065067d3a4152d8da05a22fb00f7486/readmeImgs/step3-1.png -------------------------------------------------------------------------------- /scripts/editor.js: -------------------------------------------------------------------------------- 1 | window.onload = () => { 2 | const option = { 3 | action: "editorWindowOnload", 4 | }; 5 | chrome.runtime.sendMessage(option) 6 | } 7 | 8 | let activeImg = 0; 9 | let activePosition = [{ x: 0, y: 0 }]; 10 | 11 | function updateImgs() { 12 | let imgs = app.getElementsByClassName("screenshotImg"); 13 | let offset = document.getElementById("offset").value; 14 | for (let i = 0; i < imgs.length; i++) { 15 | /** 16 | * @type {HTMLImageElement} img 17 | */ 18 | let img = imgs[i]; 19 | img.style.zIndex = 1000 - i; 20 | 21 | img.addEventListener("contextmenu", (e) => { 22 | e.preventDefault(); 23 | 24 | let imgs = app.getElementsByClassName("screenshotImg"); 25 | for (let i = 0; i < imgs.length; i++) { 26 | imgs[i].style.outline = "none"; 27 | imgs[i].style.borderRadius = "0px"; 28 | } 29 | 30 | document.getElementById("setCustomOffset").style.display = "none"; 31 | 32 | img.style.outline = "5px solid #2453c2"; 33 | img.style.borderRadius = "5px"; 34 | let contextMenu = document.getElementById("menu"); 35 | contextMenu.style.display = "flex"; 36 | contextMenu.style.left = e.clientX + "px"; 37 | if (e.clientY + parseInt((contextMenu.getBoundingClientRect()).height) > window.innerHeight) { 38 | contextMenu.style.top = (window.innerHeight - (contextMenu.getBoundingClientRect()).height) + "px" 39 | } else { 40 | contextMenu.style.top = e.clientY + "px"; 41 | } 42 | 43 | activeImg = i; 44 | activePosition = [{ x: e.clientX, y: e.clientY }]; 45 | }); 46 | } 47 | document.getElementById("app").style.setProperty("--imageOffset", (216 - offset) + "px"); 48 | } 49 | 50 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 51 | switch (request.action) { 52 | case "getVideoData": 53 | let img = document.createElement("img"); 54 | img.className = "screenshotImg"; 55 | img.src = request.videoData; 56 | document.getElementById("app").appendChild(img); 57 | updateImgs(); 58 | break; 59 | case "checkEditorOpened": 60 | sendResponse(1) 61 | break 62 | } 63 | 64 | sendResponse({ farewell: "thanks for sending! goodbye" }); 65 | }); 66 | 67 | document.getElementById("offset").min = document.getElementById("min-offset").value; 68 | document.getElementById("min-offset").max = document.getElementById("offset").value; 69 | document.getElementById("offset").max = document.getElementById("max-offset").value; 70 | document.getElementById("max-offset").min = document.getElementById("offset").value; 71 | 72 | document.getElementById("min-offset").addEventListener("input", () => { 73 | document.getElementById("offset").min = document.getElementById("min-offset").value; 74 | document.getElementById("max-offset").max = document.getElementById("offset").value; 75 | document.getElementById("max-offset").min = document.getElementById("offset").value; 76 | }); 77 | 78 | document.getElementById("max-offset").addEventListener("input", () => { 79 | document.getElementById("offset").max = document.getElementById("max-offset").value; 80 | document.getElementById("min-offset").max = document.getElementById("offset").value; 81 | document.getElementById("min-offset").min = document.getElementById("offset").value; 82 | }); 83 | 84 | document.getElementById("offset").addEventListener("input", () => { 85 | document.getElementById("app").style.setProperty("--imageOffset", (216 - document.getElementById("offset").value) + "px"); 86 | document.getElementById("min-offset").max = document.getElementById("offset").value; 87 | document.getElementById("max-offset").min = document.getElementById("offset").value; 88 | }); 89 | 90 | let canvasBlob; 91 | async function generate() { 92 | return (new Promise((resole, reject) => { 93 | try { 94 | html2canvas(document.getElementById("app"), { backgroundColor: null, scale: parseInt(document.getElementById("scale").value) }).then(function (canvas) { 95 | document.getElementById("preview").innerHTML = ""; 96 | document.getElementById("preview").appendChild(canvas); 97 | canvas.toBlob(function (blob) { 98 | canvasBlob = blob; 99 | resole(1) 100 | }); 101 | }); 102 | } catch { 103 | reject(0) 104 | } 105 | })) 106 | } 107 | document.getElementById("generate").addEventListener("click", generate); 108 | 109 | async function copyImg() { 110 | if (!canvasBlob) { 111 | console.log(await generate()) 112 | } 113 | 114 | const item = new ClipboardItem({ "image/png": canvasBlob }); 115 | navigator.clipboard.write([item]).then(function () { 116 | console.log("Image copied to clipboard"); 117 | }).catch(function (error) { 118 | console.error("Unable to write to clipboard. Error:", error); 119 | }); 120 | } 121 | document.getElementById("copyImg").addEventListener("click", copyImg); 122 | 123 | async function downloadImg() { 124 | if (!canvasBlob) { 125 | console.log(await generate()) 126 | } 127 | 128 | const a = document.createElement("a"); 129 | document.body.appendChild(a); 130 | a.href = URL.createObjectURL(canvasBlob); 131 | let date = new Date(); 132 | let year = date.getFullYear(); 133 | let month = date.getMonth() + 1; 134 | let day = date.getDate(); 135 | let hour = date.getHours(); 136 | let min = date.getMinutes(); 137 | let sec = date.getSeconds(); 138 | a.download = `screenshot_${year}${month}${day}_${hour}${min}${sec}.png`; 139 | a.click(); 140 | document.body.removeChild(a); 141 | } 142 | document.getElementById("downloadImg").addEventListener("click", downloadImg); 143 | 144 | // menu 145 | document.body.addEventListener("click", (e) => { 146 | if ((e.target.className.includes("menuArea"))) return; 147 | console.log(e.target.className); 148 | if (document.getElementById("menu").style.display !== "none") { 149 | document.getElementById("menu").style.display = "none"; 150 | 151 | if (document.getElementById("setCustomOffset").style.display === "none") { 152 | let app = document.getElementById("app"); 153 | let imgs = app.getElementsByClassName("screenshotImg"); 154 | for (let i = 0; i < imgs.length; i++) { 155 | imgs[i].style.outline = "none"; 156 | imgs[i].style.borderRadius = "0px"; 157 | } 158 | } 159 | } else if (document.getElementById("setCustomOffset").style.display !== "none") { 160 | document.getElementById("setCustomOffset").style.display = "none"; 161 | 162 | let app = document.getElementById("app"); 163 | let imgs = app.getElementsByClassName("screenshotImg"); 164 | for (let i = 0; i < imgs.length; i++) { 165 | imgs[i].style.outline = "none"; 166 | imgs[i].style.borderRadius = "0px"; 167 | } 168 | } 169 | }); 170 | 171 | document.getElementById("upImg").addEventListener("click", () => { 172 | if (activeImg > 0) { 173 | let app = document.getElementById("app"); 174 | let imgs = app.children; 175 | let img = imgs[activeImg]; 176 | let prevImg = imgs[activeImg - 1]; 177 | app.insertBefore(img, prevImg); 178 | updateImgs(); 179 | } 180 | }); 181 | document.getElementById("downImg").addEventListener("click", () => { 182 | if (activeImg < app.children.length - 1) { 183 | let app = document.getElementById("app"); 184 | let imgs = app.children; 185 | let img = imgs[activeImg]; 186 | let nextImg = imgs[activeImg + 1]; 187 | app.insertBefore(nextImg, img); 188 | updateImgs(); 189 | } 190 | }); 191 | document.getElementById("topImg").addEventListener("click", () => { 192 | if (activeImg > 0) { 193 | let app = document.getElementById("app"); 194 | let imgs = app.children; 195 | let img = imgs[activeImg]; 196 | app.insertBefore(img, imgs[0]); 197 | updateImgs(); 198 | } 199 | }); 200 | document.getElementById("bottomImg").addEventListener("click", () => { 201 | if (activeImg < app.children.length - 1) { 202 | let app = document.getElementById("app"); 203 | let imgs = app.children; 204 | let img = imgs[activeImg]; 205 | app.appendChild(img); 206 | updateImgs(); 207 | } 208 | }); 209 | 210 | // let lestSetCustomOffsetValue = document.querySelectorAll(".screenshotImg")[activeImg].style.getPropertyValue("--imageOffset"); 211 | let lestSetCustomOffsetValue = 48; 212 | document.getElementById("customOffset").addEventListener("click", (e) => { 213 | lestSetCustomOffsetValue = document.getElementById("customOffsetValue").value; 214 | document.getElementById("lastOffsetValue").innerText = lestSetCustomOffsetValue; 215 | 216 | document.getElementById("customOffsetValue").value = 217 | document.querySelectorAll(".screenshotImg")[activeImg].style.getPropertyValue("--imageOffset") 218 | ? 219 | 216 - parseInt(document.querySelectorAll(".screenshotImg")[activeImg].style.getPropertyValue("--imageOffset")) 220 | : 221 | 216 - parseInt(document.getElementById("app").style.getPropertyValue("--imageOffset")); 222 | 223 | let setCustomOffset = document.getElementById("setCustomOffset") 224 | setCustomOffset.style.display = "block"; 225 | setCustomOffset.style.left = activePosition[0].x + "px"; 226 | console.log(parseInt((setCustomOffset.getBoundingClientRect()).height)) 227 | if (activePosition[0].y + parseInt((setCustomOffset.getBoundingClientRect()).height) > window.innerHeight) { 228 | setCustomOffset.style.top = (window.innerHeight - (setCustomOffset.getBoundingClientRect()).height) + "px" 229 | } else { 230 | setCustomOffset.style.top = activePosition[0].y + "px"; 231 | // setCustomOffset.style.top = e.clientY + "px"; 232 | } 233 | }); 234 | document.getElementById("lastOffset").addEventListener("click", () => { 235 | document.getElementById("customOffsetValue").value = lestSetCustomOffsetValue; 236 | document.getElementById("customOffsetValue").dispatchEvent(new Event("input")); 237 | }); 238 | document.getElementById("showAll").addEventListener("click", () => { 239 | document.getElementById("customOffsetValue").value = 216; 240 | document.getElementById("customOffsetValue").dispatchEvent(new Event("input")); 241 | }); 242 | document.getElementById("customOffsetValue").addEventListener("input", () => { 243 | document.querySelectorAll(".screenshotImg")[activeImg].style.setProperty("--imageOffset", (216 - document.getElementById("customOffsetValue").value) + "px"); 244 | }); 245 | document.getElementById("deleteImg").addEventListener("click", () => { 246 | let imgs = app.getElementsByClassName("screenshotImg"); 247 | imgs[activeImg].remove(); 248 | updateImgs(); 249 | }); 250 | 251 | 252 | let lastScrollTop = 0; 253 | window.addEventListener("scroll", () => { 254 | let dy = window.scrollY - lastScrollTop; 255 | console.log(dy); 256 | let menus = document.getElementsByClassName("menu"); 257 | for (let i = 0; i < menus.length; i++) { 258 | let menu = menus[i]; 259 | let top = parseInt(menu.style.top); 260 | menu.style.top = (top - dy) + "px"; 261 | } 262 | lastScrollTop = window.scrollY; 263 | }); 264 | -------------------------------------------------------------------------------- /scripts/popup.js: -------------------------------------------------------------------------------- 1 | document.getElementById("screenshot").addEventListener('click', () => { 2 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 3 | const tab = tabs[0]; 4 | 5 | function getVideoScreenshot() { 6 | v = document.querySelector('video'); 7 | c = document.createElement('canvas'); 8 | c.height = v.videoHeight || parseInt(v.style.height); 9 | c.width = v.videoWidth || parseInt(v.style.width); 10 | ctx = c.getContext('2d'); 11 | ctx.drawImage(v, 0, 0); 12 | c.toDataURL() 13 | 14 | let option = { 15 | action: "getVideoData", 16 | videoData: c.toDataURL() 17 | }; 18 | 19 | chrome.runtime.sendMessage(option); 20 | }; 21 | 22 | chrome.scripting.executeScript({ 23 | target: { tabId: tab.id }, 24 | func: getVideoScreenshot, 25 | 26 | }).then(() => console.log('擷取關鍵影格')); 27 | }); 28 | }); 29 | 30 | document.getElementById("popEditor").addEventListener('click', () => { 31 | chrome.windows.create({ 32 | url: chrome.runtime.getURL("./popup/editor.html"), 33 | type: "popup", 34 | width: 898, 35 | height: 400, 36 | focused: true, 37 | }); 38 | window.top = window; 39 | }); 40 | 41 | document.getElementById("howToUse").addEventListener("click", () => { 42 | chrome.tabs.create({ url: chrome.runtime.getURL("./popup/help.html") }); 43 | }) 44 | 45 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 46 | switch (request.action) { 47 | case "getVideoData": 48 | console.log("getVideoData"); 49 | console.log(request.videoData); 50 | document.getElementById("screenshotImg").src = request.videoData; 51 | break; 52 | } 53 | 54 | }); --------------------------------------------------------------------------------