您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Twitch のコメントをニコニコ風にスクロールさせます。
当前为
// ==UserScript== // @name Twitch Screen Comment Scroller(fix) // @namespace page.loupe.tsscf // @description Twitch のコメントをニコニコ風にスクロールさせます。 // @match https://www.twitch.tv/* // @version 0.4.1 // @require https://openuserjs.org/src/libs/sizzle/GM_config.js // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @author knoa // @author mikan-megane // ==/UserScript== (function () { /* カスタマイズ(設定画面での変更が優先されます) */ var SCRIPTNAME = 'ScreenCommentScroller'; var COLOR = '#ffffff';/*コメント色*/ var OCOLOR = '#000000';/*コメント縁取り色*/ var OWIDTH = 1 / 10;/*コメント縁取りの太さ(比率)*/ var OPACITY = '0.25';/*コメントの不透明度*/ var MAXLINES = 10;/*コメント最大行数*/ var LINEHEIGHT = 1.2;/*コメント行高さ*/ var DURATION = 5;/*スクロール秒数*/ var FPS = 60;/*秒間コマ数*/ var EMOJI = true;/*絵文字のみのコメントの際絵文字を1つだけ表示するか(beta)*/ var EMOJISIZE = 1.5;/*絵文字の大きさ(beta)*/ /* サイト定義 */ var site = { getScreen: () => document.querySelector('.video-player__container'), getBoard: () => document.querySelector('.chat-scrollable-area__message-container') || document.querySelector('.video-chat__message-list-wrapper ul'),/*live || log*/ getComments: (node) => node.querySelector('[data-a-target="chat-line-message-body"],.video-chat__message'), getVideo: () => document.querySelector('video'), }; /* 処理本体 */ var screen, board, video, canvas, context, lines = [], fontsize, scrollCommentsTimer, title = document.title,configEdit = false,commentObserver; var core = { /* DOMの初期化待ち&ページ変更検知 */ waitStart() { window.setInterval(function () { var screen_ = site.getScreen(); var board_ = site.getBoard(); var video_ = site.getVideo(); var title_ = document.title; // console.debug('wait comment list...', { screen_, board_, video_ }); if (screen_ && board_ && video_ && (screen_ != screen || board_ != board || video_ != video || title_ != title || configEdit || canvas.width != screen.offsetWidth)) { screen = screen_; board = board_; video = video_; title = title_; configEdit = false; core.initialize(); } }, 3000); }, /* 初期化 */ initialize() { /* コメントをスクロールさせるCanvasの設置 */ /* (描画処理の軽さは HTML5 Canvas, CSS Position Left, CSS Transition の順) */ document.querySelector('canvas#' + SCRIPTNAME)?.remove(); canvas = document.createElement('canvas'); canvas.id = SCRIPTNAME; screen.appendChild(canvas); context = canvas.getContext('2d'); /* メイン処理 */ core.addStyle(); core.modify(); core.listenComments(); core.scrollComments(); }, /* *スクリーンサイズに変化があればcanvasも変化させる* */ modify() { canvas.width = screen.offsetWidth; canvas.height = screen.offsetHeight; fontsize = (canvas.height / MAXLINES) / LINEHEIGHT; context.font = 'bold ' + (fontsize) + 'px sans-serif'; context.fillStyle = COLOR; context.strokeStyle = OCOLOR; context.lineWidth = fontsize * OWIDTH; }, /* スタイル付与 */ addStyle() { let head = document.querySelector('head'); if (!head) return; document.querySelector('style#' + SCRIPTNAME + 'Style')?.remove(); let style = document.createElement('style'); style.type = 'text/css'; style.id = SCRIPTNAME + 'Style'; style.innerHTML = '' + 'canvas#' + SCRIPTNAME + '{' + ' pointer-events: none;' + ' position: absolute;' + ' top: 0;' + ' left: 0;' + ' width: 100%;' + ' height: 100%;' + ' opacity: ' + OPACITY + ';' + ' z-index: 99999;' + '}' + ''; head.appendChild(style); }, /* コメントの新規追加を見守るイベント */ listenComments() { if (commentObserver) { commentObserver.disconnect() } commentObserver = new MutationObserver(function (mutations) { mutations.forEach(function (mutation) { if (mutation.type == 'childList') { mutation.addedNodes.forEach(function (node) { core.attachComment(site.getComments(node)); }); } }); }) commentObserver.observe(board, { childList: true }); }, /* コメントが追加されるたびにスクロールキューに追加 */ attachComment(comment) { let record = {}; record.text = comment.querySelector('[data-a-target="chat-message-text"]').textContent.trim();/*流れる文字列*/ record.img = comment.querySelector('img:first-child');/*絵文字*/ record.width = record.text ? context.measureText(record.text).width : context.measureText('絵').width * EMOJISIZE;/*文字列の幅*/ record.life = DURATION * FPS;/*文字列が消えるまでのコマ数*/ record.left = canvas.width;/*左端からの距離*/ record.delta = (canvas.width + record.width) / (record.life);/*コマあたり移動距離*/ record.reveal = record.width / record.delta;/*文字列が右端から抜けてあらわになるまでのコマ数*/ record.touch = canvas.width / record.delta;/*文字列が左端に触れるまでのコマ数*/ /* 追加されたコメントをどの行に流すかを決定する */ for (let i = 0; i < MAXLINES; i++) { let length = lines[i] ? lines[i].length : 0;/*同じ行に詰め込まれているコメント数*/ switch (true) { /* 行が空いていれば追加 */ case (lines[i] == undefined || !length): lines[i] = []; /* 以前のコメントより長い(速い)文字列なら、左端に到達する時間で判断する */ case (lines[i][length - 1].reveal < 0 && lines[i][length - 1].delta > record.delta): /* 以前のコメントより短い(遅い)文字列なら、右端から姿を見せる時間で判断する */ case (lines[i][length - 1].life < record.touch && lines[i][length - 1].delta < record.delta): /*条件に当てはまればすべてswitch文のあとの処理で行に追加*/ break; default: /*条件に当てはまらなければ次の行に入れられるかの判定へ*/ continue; } record.top = ((canvas.height / MAXLINES) * i) + fontsize; lines[i].push(record); break; } }, /* FPSタイマー駆動 */ scrollComments() { if (scrollCommentsTimer) { window.clearInterval(scrollCommentsTimer); } scrollCommentsTimer = window.setInterval(function () { /* 再生中じゃなければ処理しない */ if (video.paused) return; /* Canvas描画 */ context.clearRect(0, 0, canvas.width, canvas.height); for (let i = 0; lines[i]; i++) { for (let j = 0; lines[i][j]; j++) { if(lines[i][j].text){ /*視認性を向上させるスクロール文字の縁取りは、幸いにもパフォーマンスにほぼ影響しない*/ context.strokeText(lines[i][j].text, lines[i][j].left, lines[i][j].top); context.fillText(lines[i][j].text, lines[i][j].left, lines[i][j].top); } else if (lines[i][j].img && EMOJI) { context.drawImage(lines[i][j].img, lines[i][j].left, lines[i][j].top, fontsize * EMOJISIZE, fontsize * EMOJISIZE); } lines[i][j].life--; lines[i][j].reveal--; lines[i][j].touch--; lines[i][j].left -= lines[i][j].delta; } if (lines[i][0] && lines[i][0].life == 0) { lines[i].shift(); } } }, 1000 / FPS); }, /* 設定画面 */ settings() { GM.registerMenuCommand("設定", () => GM_config.open()); const config_init = () => { COLOR = GM_config.get('COLOR') || COLOR; OCOLOR = GM_config.get('OCOLOR') || OCOLOR; OWIDTH = GM_config.get('OWIDTH') || OWIDTH; OPACITY = GM_config.get('OPACITY') || OPACITY; MAXLINES = GM_config.get('MAXLINES') || MAXLINES; LINEHEIGHT = GM_config.get('LINEHEIGHT') || LINEHEIGHT; DURATION = GM_config.get('DURATION') || DURATION; FPS = GM_config.get('FPS') || FPS; EMOJI = GM_config.get('EMOJI') || EMOJI; EMOJISIZE = GM_config.get('EMOJISIZE') || EMOJISIZE; configEdit = true; } GM_config.init({ 'id': 'ScreenCommentScroller', 'title': 'ScreenCommentScroller', 'fields': { 'COLOR': { 'label': 'コメント色', 'type': 'text', 'default': COLOR }, 'OCOLOR': { 'label': 'コメント縁取り色', 'type': 'text', 'default': OCOLOR }, 'OWIDTH': { 'label': 'コメント縁取りの太さ(比率)', 'type': 'float', 'default': OWIDTH }, 'OPACITY': { 'label': 'コメントの不透明度', 'type': 'float', 'default': OPACITY }, 'MAXLINES': { 'label': 'コメント最大行数', 'type': 'int', 'default': MAXLINES }, 'LINEHEIGHT': { 'label': 'コメント行高さ', 'type': 'float', 'default': LINEHEIGHT }, 'DURATION': { 'label': 'スクロール秒数', 'type': 'float', 'default': DURATION }, 'FPS': { 'label': '秒間コマ数', 'type': 'int', 'default': FPS }, 'EMOJI': { 'label': '絵文字のみのコメントの際絵文字を1つだけ表示するか(beta)', 'type': 'checkbox', 'default': EMOJI }, 'EMOJISIZE': { 'label': '絵文字の大きさ(beta)', 'type': 'float', 'default': EMOJISIZE }, }, 'events': { 'init': () => config_init(), 'save': () => config_init(), }, }); } }; core.settings(); core.waitStart(); })();