Twitch Screen Comment Scroller

Twitch のコメントをニコニコ風にスクロールさせます。

  1. // ==UserScript==
  2. // @name Twitch Screen Comment Scroller
  3. // @namespace knoa.jp
  4. // @description Twitch のコメントをニコニコ風にスクロールさせます。
  5. // @include https://www.twitch.tv/*
  6. // @version 0.2.1
  7. // @grant none
  8. // ==/UserScript==
  9.  
  10. (function(){
  11. /* カスタマイズ */
  12. var SCRIPTNAME = 'ScreenCommentScroller';
  13. var COLOR = '#ffffff';/*コメント色*/
  14. var OCOLOR = '#000000';/*コメント縁取り色*/
  15. var OWIDTH = 1/10;/*コメント縁取りの太さ(比率)*/
  16. var OPACITY = '0.25';/*コメントの不透明度*/
  17. var MAXLINES = 10;/*コメント最大行数*/
  18. var LINEHEIGHT = 1.2;/*コメント行高さ*/
  19. var DURATION = 5;/*スクロール秒数*/
  20. var FPS = 60;/*秒間コマ数*/
  21. /* サイト定義 */
  22. var site = {
  23. getScreen: function(){return document.querySelector('.video-player__container')},
  24. getBoard: function(){return document.querySelector('[role="log"]') || document.querySelector('.video-chat__message-list-wrapper ul')},/*live || log*/
  25. getComments: function(node){return node.querySelector('[data-a-target="chat-message-text"]')},
  26. //getPlay: function(){return document.querySelector('.player-button')},
  27. getVideo: function(){return document.querySelector('video[src]')},
  28. };
  29. /* 処理本体 */
  30. let retry = 10;
  31. var screen, board, play, video, canvas, context, lines = [], fontsize;
  32. var core = {
  33. /* 初期化 */
  34. initialize: function(){
  35. console.log(SCRIPTNAME, 'initialize...');
  36. /* 主要要素が取得できるまで読み込み待ち */
  37. screen = site.getScreen();
  38. board = site.getBoard();
  39. //play = site.getPlay();
  40. video = site.getVideo();
  41. console.log(screen, board, video);
  42. if(!screen || !board || !video){
  43. window.setTimeout(function(){
  44. if(retry--) core.initialize();
  45. }, 1000);
  46. return;
  47. }
  48. /* コメントをスクロールさせるCanvasの設置 */
  49. /* (描画処理の軽さは HTML5 Canvas, CSS Position Left, CSS Transition の順) */
  50. canvas = document.createElement('canvas');
  51. canvas.id = SCRIPTNAME;
  52. screen.appendChild(canvas);
  53. context = canvas.getContext('2d');
  54. /* メイン処理 */
  55. core.addStyle();
  56. core.listenComments();
  57. core.scrollComments();
  58. /**/
  59. window.addEventListener('popstate', function(){
  60. core.initialize();
  61. });
  62. document.body.addEventListener('DOMAttrModified', function(){
  63. if(video.src == site.getVideo().src) return;
  64. core.initialize();
  65. });
  66. },
  67. /* *スクリーンサイズに変化があればcanvasも変化させる* */
  68. modify: function(){
  69. if(canvas.width == screen.offsetWidth) return;
  70. //console.log(SCRIPTNAME, 'modify...');
  71. canvas.width = screen.offsetWidth;
  72. canvas.height = screen.offsetHeight;
  73. fontsize = (canvas.height / MAXLINES) / LINEHEIGHT;
  74. context.font = 'bold ' + (fontsize) + 'px sans-serif';
  75. context.fillStyle = COLOR;
  76. context.strokeStyle = OCOLOR;
  77. context.lineWidth = fontsize * OWIDTH;
  78. },
  79. /* スタイル付与 */
  80. addStyle: function(){
  81. //console.log(SCRIPTNAME, 'addStyle...');
  82. let head = document.getElementsByTagName('head')[0];
  83. if (!head) return;
  84. let style = document.createElement('style');
  85. style.type = 'text/css';
  86. style.innerHTML = ''+
  87. 'canvas#'+SCRIPTNAME+'{' +
  88. ' pointer-events: none;' +
  89. ' position: absolute;' +
  90. ' top: 0;' +
  91. ' left: 0;' +
  92. ' width: 100%;' +
  93. ' height: 100%;' +
  94. ' opacity: '+OPACITY+';' +
  95. ' z-index: 99999;' +
  96. '}'+
  97. '';
  98. head.appendChild(style);
  99. },
  100. /* コメントの新規追加を見守る */
  101. listenComments: function(){
  102. //console.log(SCRIPTNAME, 'listenComments...', board);
  103. board.addEventListener('DOMNodeInserted', function(e){
  104. //console.log(e);
  105. let comment = e.target;
  106. core.modify();
  107. core.attachComment(site.getComments(comment));
  108. });
  109. },
  110. /* コメントが追加されるたびにスクロールキューに追加 */
  111. attachComment: function(comment){
  112. //console.log(SCRIPTNAME, 'attachComment...', comment);
  113. let record = {};
  114. record.text = comment.textContent;/*流れる文字列*/
  115. record.width = context.measureText(record.text).width;/*文字列の幅*/
  116. record.life = DURATION * FPS;/*文字列が消えるまでのコマ数*/
  117. record.left = canvas.width;/*左端からの距離*/
  118. record.delta = (canvas.width + record.width) / (record.life);/*コマあたり移動距離*/
  119. record.reveal = record.width / record.delta;/*文字列が右端から抜けてあらわになるまでのコマ数*/
  120. record.touch = canvas.width / record.delta;/*文字列が左端に触れるまでのコマ数*/
  121. /* 追加されたコメントをどの行に流すかを決定する */
  122. for(let i=0; i<MAXLINES; i++){
  123. let length = lines[i] ? lines[i].length : 0;/*同じ行に詰め込まれているコメント数*/
  124. switch(true){
  125. /* 行が空いていれば追加 */
  126. case(lines[i] == undefined || !length):
  127. lines[i] = [];
  128. /* 以前のコメントより長い(速い)文字列なら、左端に到達する時間で判断する */
  129. case(lines[i][length - 1].reveal < 0 && lines[i][length - 1].delta > record.delta):
  130. /* 以前のコメントより短い(遅い)文字列なら、右端から姿を見せる時間で判断する */
  131. case(lines[i][length - 1].life < record.touch && lines[i][length - 1].delta < record.delta):
  132. /*条件に当てはまればすべてswitch文のあとの処理で行に追加*/
  133. break;
  134. default:
  135. /*条件に当てはまらなければ次の行に入れられるかの判定へ*/
  136. continue;
  137. }
  138. record.top = ((canvas.height / MAXLINES) * i) + fontsize;
  139. lines[i].push(record);
  140. break;
  141. }
  142. },
  143. /* FPSタイマー駆動 */
  144. scrollComments: function(){
  145. //console.log(SCRIPTNAME, 'scrollComment...');
  146. var interval = window.setInterval(function(){
  147. /* 再生中じゃなければ処理しない */
  148. if(video.paused) return;
  149. /* Canvas描画 */
  150. context.clearRect(0, 0, canvas.width, canvas.height);
  151. for(let i=0; lines[i]; i++){
  152. for(let j=0; lines[i][j]; j++){
  153. /*視認性を向上させるスクロール文字の縁取りは、幸いにもパフォーマンスにほぼ影響しない*/
  154. context.strokeText(lines[i][j].text, lines[i][j].left, lines[i][j].top);
  155. context.fillText(lines[i][j].text, lines[i][j].left, lines[i][j].top);
  156. lines[i][j].life--;
  157. lines[i][j].reveal--;
  158. lines[i][j].touch--;
  159. lines[i][j].left -= lines[i][j].delta;
  160. }
  161. if(lines[i][0] && lines[i][0].life == 0){
  162. lines[i].shift();
  163. }
  164. }
  165. }, 1000/FPS);
  166. },
  167. };
  168. core.initialize();
  169. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址