|
// ==UserScript== |
|
// @name 巴哈姆特動畫瘋影片擷圖小工具 |
|
// @namespace http://tampermonkey.net/ |
|
// @version 2.0 |
|
// @description video screenshot on ani.gamer.com.tw |
|
// @author Rplus |
|
// @match https://ani.gamer.com.tw/animeVideo.php?sn=* |
|
// @grant GM_registerMenuCommand |
|
// ==/UserScript== |
|
|
|
(function() { |
|
let video = getVideo(); |
|
let title = ''; |
|
let option = { |
|
fileExt: 'jpg', |
|
mimeType: 'image/jpeg', |
|
compressRatio: 0.95, |
|
}; |
|
|
|
unsafeWindow.addEventListener('load', () => { |
|
video = getVideo(); |
|
if (video) { |
|
init(); |
|
} else { |
|
console.log('load video GG, wait 5s'); |
|
delayInit(); |
|
} |
|
}); |
|
|
|
function delayInit() { |
|
setTimeout(() => { |
|
if (!unsafeWindow.videojs) { |
|
delayInit(); |
|
} else { |
|
init(); |
|
} |
|
}, 1000); |
|
} |
|
|
|
function init() { |
|
video = getVideo(); |
|
title = document.querySelector('h1')?.textContent || document.title; |
|
|
|
console.log('load'); |
|
if (!video) { return; } |
|
|
|
document.addEventListener('keydown', handleKeyDown); |
|
|
|
injectScreenshotBtn(); |
|
} |
|
|
|
function handleKeyDown(e) { |
|
if (!e.altKey) { return; } |
|
|
|
switch (e.code) { |
|
case 'KeyS': |
|
screenshot(video, title); |
|
break; |
|
case 'Equal': |
|
shiftVideoFrame(1); |
|
break; |
|
case 'Minus': |
|
shiftVideoFrame(-1); |
|
break; |
|
default: |
|
break; |
|
} |
|
} |
|
|
|
function shiftVideoFrame(dir = 1) { |
|
video.currentTime += (dir / 60); |
|
} |
|
|
|
function injectScreenshotBtn() { |
|
const bar = document.querySelector('.control-bar-rightbtn'); |
|
|
|
if (!bar) { return; } |
|
const btn = document.createElement('div'); |
|
btn.className = 'vjs-button vjs-control vjs-playback-rate'; |
|
btn.innerHTML = `<div class="vjs-playback-rate-value">擷圖</div>` |
|
btn.addEventListener('click', () => screenshot(video, title)); |
|
bar.appendChild(btn); |
|
} |
|
|
|
function screenshot(video, title) { |
|
const currentTimeStr = new Date(video.currentTime * 1000).toISOString().slice(11, 19).replace(/\:/g, '-'); |
|
const fn = title + '_' + currentTimeStr + '.' + option.fileExt; |
|
saveImage(getImgDataUrl(video), fn); |
|
} |
|
|
|
function getVideo() { |
|
return document.getElementById('ani_video_html5_api') || document.querySelector('video'); |
|
} |
|
|
|
function getImgDataUrl(videoEl, scale = unsafeWindow.devicePixelRatio || 1) { |
|
const canvas = document.createElement('canvas'); |
|
canvas.width = videoEl.videoWidth * scale; |
|
canvas.height = videoEl.videoHeight * scale; |
|
canvas.getContext('2d').drawImage(videoEl, 0, 0, canvas.width, canvas.height); |
|
|
|
return canvas.toDataURL(option.mimeType, option.compressRatio); |
|
} |
|
|
|
function saveImage(imgSrc, filename) { |
|
var link = document.createElement('a'); |
|
link.href = imgSrc; |
|
link.target = '_img'; |
|
link.download = filename; |
|
document.body.appendChild(link); |
|
link.click(); |
|
document.body.removeChild(link); |
|
} |
|
|
|
GM_registerMenuCommand('存檔格式設定為 JPG', updateConfig('jpg'), 'J'); |
|
GM_registerMenuCommand('存檔格式設定為 PNG', updateConfig('png'), 'P'); |
|
GM_registerMenuCommand('調整圖片壓縮率', udpateCompressRatio, 'C'); |
|
|
|
function updateConfig(type) { |
|
return () => { |
|
switch (type) { |
|
case 'jpg': |
|
option.fileExt = 'jpg'; |
|
option.mimeType = 'image/jpeg'; |
|
break; |
|
case 'png': |
|
option.fileExt = 'png'; |
|
option.mimeType = 'image/png'; |
|
break; |
|
default: |
|
break; |
|
} |
|
}; |
|
} |
|
|
|
function udpateCompressRatio() { |
|
let value = unsafeWindow.prompt(`調整圖片壓縮率(0: 最差,1: 最佳)`, option.compressRatio); |
|
if (!isNaN(value)) { |
|
value = Number(value) |
|
if (value <= 1 && value >= 0) { |
|
option.compressRatio = value; |
|
} else { |
|
unsafeWindow.alert(`輸入錯誤壓縮率: ${value}。\n請使用合格數字區間`); |
|
} |
|
} else { |
|
unsafeWindow.alert(`輸入錯誤壓縮率: ${value}。\n請使用數字格式`); |
|
} |
|
} |
|
})(); |
@Rplus 感謝解釋,雖然瞭解 jpg 相比 png 是較為輕量的選擇,即便在「動畫截圖」這個情境下 png 的優勢較不明顯,但我想有些使用者還是比較偏好 png 格式。雖然動手修 code 不算太難,但是否能提供使用者選擇 png 的選項? 不知道能不能做到提供參數設定,或者再維護一份專門存 png 的 gist