Comment Scroller Revised Version

OPENREC.tv KICK のコメントをニコニコ風にスクロールさせます。

  1. // ==UserScript==
  2. // @name Comment Scroller Revised Version
  3. // @namespace kene
  4. // @description OPENREC.tv KICK のコメントをニコニコ風にスクロールさせます。
  5. // @match https://www.openrec.tv/live/*
  6. // @match https://kick.com/*
  7. // @version 1.0.0
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function(){
  12. /* カスタマイズ */
  13. var SCRIPTNAME = 'ScreenCommentScrollerRevisedVersion';
  14. var COLOR = '#ffffff';/*コメント色*/
  15. var OCOLOR = '#000000';/*コメント縁取り色*/
  16. var OWIDTH = 1/10;/*コメント縁取りの太さ(比率)*/
  17. var OPACITY = '0.50';/*コメントの不透明度*/
  18. var MAXLINES = 15;/*コメント最大行数*/
  19. var LINEHEIGHT = 1.4;/*コメント行高さ*/
  20. var DURATION = 5;/*スクロール秒数*/
  21. var FPS = 60;/*秒間コマ数*/
  22.  
  23. /* サイト定義 */
  24. var url = window.location.href;
  25. var screen_query = '';
  26. var board_query = "";
  27. var chat_query = "";
  28. if ( url.match(new RegExp(/www.openrec.tv/)) != null ) {
  29. /* OPENREC */
  30. screen_query = '.video-player-wrapper';
  31. board_query = '.chat-list-content';
  32. chat_query = '.chat-content';
  33. } else if ( url.match(new RegExp(/kick.com/)) != null ) {
  34. /* KICK */
  35. screen_query = '#injected-embedded-channel-player-video';
  36. board_query = '#chatroom-messages div:first-child';
  37. chat_query = 'span:last-child';
  38. } else {
  39. /* 未対応サイト */
  40. return;
  41. }
  42.  
  43. /* 処理本体 */
  44. var screen, board, canvas, context, lines = [], fontsize;
  45. var core = {
  46. /* 初期化 */
  47. initialize: function(){
  48. console.log(SCRIPTNAME, 'initialize...');
  49. /* 主要要素が取得できるまで読み込み待ち */
  50. screen = document.querySelector(screen_query)
  51. board = document.querySelector(board_query);
  52. if(!screen || !board){
  53. window.setTimeout(function(){
  54. core.initialize();
  55. }, 1000);
  56. return;
  57. }
  58. /* コメントをスクロールさせるCanvasの設置 */
  59. /* (描画処理の軽さは HTML5 Canvas, CSS Position Left, CSS Transition の順) */
  60. canvas = document.createElement('canvas');
  61. canvas.id = SCRIPTNAME;
  62. screen.appendChild(canvas);
  63. context = canvas.getContext('2d');
  64. /* メイン処理 */
  65. core.addStyle();
  66. core.listenComments();
  67. core.scrollComments();
  68. },
  69. /* *スクリーンサイズに変化があればcanvasも変化させる* */
  70. modify: function(){
  71. if(canvas.width == screen.offsetWidth) return;
  72. canvas.width = screen.offsetWidth;
  73. canvas.height = screen.offsetHeight;
  74. fontsize = (canvas.height / MAXLINES) / LINEHEIGHT;
  75. context.font = 'bold ' + (fontsize) + 'px sans-serif';
  76. context.fillStyle = COLOR;
  77. context.strokeStyle = OCOLOR;
  78. context.lineWidth = fontsize * OWIDTH;
  79. },
  80. /* スタイル付与 */
  81. addStyle: function(){
  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. var mo = new MutationObserver(function(mutationRecords){
  103. core.modify();
  104. mutationRecords.forEach((mutation) => {
  105. if ( mutation.type != "childList" ) {
  106. return;
  107. }
  108. mutation.addedNodes.forEach( el => {
  109. var parser = new DOMParser();
  110. var dom = parser.parseFromString(el.innerHTML, "text/html");
  111. core.attachComment(dom.querySelector(chat_query));
  112. });
  113. });
  114. });
  115. mo.observe(board,{childList: true});
  116. },
  117. /* コメントが追加されるたびにスクロールキューに追加 */
  118. attachComment: function(comment){
  119. let record = {};
  120. record.text = comment.textContent;/*流れる文字列*/
  121. record.width = context.measureText(record.text).width;/*文字列の幅*/
  122. record.life = DURATION * FPS;/*文字列が消えるまでのコマ数*/
  123. record.left = canvas.width;/*左端からの距離*/
  124. record.delta = (canvas.width + record.width) / (record.life);/*コマあたり移動距離*/
  125. record.reveal = record.width / record.delta;/*文字列が右端から抜けてあらわになるまでのコマ数*/
  126. record.touch = canvas.width / record.delta;/*文字列が左端に触れるまでのコマ数*/
  127. /* 追加されたコメントをどの行に流すかを決定する */
  128. for(let i=0; i<MAXLINES; i++){
  129. let length = lines[i] ? lines[i].length : 0;/*同じ行に詰め込まれているコメント数*/
  130. switch(true){
  131. /* 行が空いていれば追加 */
  132. case(lines[i] == undefined || !length):
  133. lines[i] = [];
  134. /* 以前のコメントより長い(速い)文字列なら、左端に到達する時間で判断する */
  135. /* fallthrough */
  136. case(lines[i][length - 1].reveal < 0 && lines[i][length - 1].delta > record.delta):
  137. /* 以前のコメントより短い(遅い)文字列なら、右端から姿を見せる時間で判断する */
  138. /* fallthrough */
  139. case(lines[i][length - 1].life < record.touch && lines[i][length - 1].delta < record.delta):
  140. /*条件に当てはまればすべてswitch文のあとの処理で行に追加*/
  141. break;
  142. default:
  143. /*条件に当てはまらなければ次の行に入れられるかの判定へ*/
  144. continue;
  145. }
  146. record.top = ((canvas.height / MAXLINES) * i) + fontsize;
  147. lines[i].push(record);
  148. break;
  149. }
  150. },
  151. /* FPSタイマー駆動 */
  152. scrollComments: function(){
  153. window.setInterval(function(){
  154. /* Canvas描画 */
  155. context.clearRect(0, 0, canvas.width, canvas.height);
  156. for(let i=0; lines[i]; i++){
  157. for(let j=0; lines[i][j]; j++){
  158. /*視認性を向上させるスクロール文字の縁取りは、幸いにもパフォーマンスにほぼ影響しない*/
  159. context.strokeText(lines[i][j].text, lines[i][j].left, lines[i][j].top);
  160. context.fillText(lines[i][j].text, lines[i][j].left, lines[i][j].top);
  161. lines[i][j].life--;
  162. lines[i][j].reveal--;
  163. lines[i][j].touch--;
  164. lines[i][j].left -= lines[i][j].delta;
  165. }
  166. if(lines[i][0] && lines[i][0].life == 0){
  167. lines[i].shift();
  168. }
  169. }
  170. }, 1000/FPS);
  171. },
  172. };
  173. core.initialize();
  174. })();

QingJ © 2025

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