HistogramHeatGraph_html5.user.js

ニコニコ動画のコメントをグラフで表示(html5版)※コメントをリロードすることでグラフを再描画します

2021-08-14 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

  1. // ==UserScript==
  2. // @name HistogramHeatGraph_html5.user.js
  3. // @namespace sotoba
  4. // @version 1.1.8.20210814
  5. // @description ニコニコ動画のコメントをグラフで表示(html5版)※コメントをリロードすることでグラフを再描画します
  6. // @homepageURL https://github.com/SotobatoNihu/HistogramHeatGraph_html5
  7. // @match https://www.nicovideo.jp/*
  8. // @match https://www.nicovideo.jp/watch/*
  9. // @require https://code.jquery.com/jquery-3.2.1.min.js
  10. // @grant none
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. 'use strict';
  15.  
  16. // default settings
  17. class NicoHeatGraph {
  18. constructor() {
  19. this.MINIMUMBARNUM = 50;
  20. this.DEFAULTINTERBAL = 10;
  21. this.MAXCOMMENTNUM = 30;
  22. this.GRAPHHEIGHT = 30;
  23. this.GRAPHDEFWIDTH = 856;
  24. this.barIndexNum = 0;
  25. this.$canvas = null;
  26. this.$commentgraph = $('<div>').attr('id', 'comment-graph');
  27. this.$commentlist = $('<div>').attr('id', 'comment-list');
  28. }
  29.  
  30. drawCoordinate() {
  31. const $commentgraph = this.$commentgraph;
  32. const $commentlist = this.$commentlist;
  33. if (!($('#comment-graph').length)) {
  34. $('.PlayerContainer').eq(0).append($commentgraph);
  35. $('.MainContainer').eq(0).append($commentlist);
  36. }
  37. this.$canvas = $(".CommentRenderer").eq(0);
  38. const styleString = `
  39. #comment-graph :hover{
  40. -webkit-filter: hue-rotate(180deg);
  41. filter: hue-rotate(180deg);
  42. }
  43. #comment-list:empty {
  44. display: none;
  45. }
  46. `;
  47. const style = document.createElement('style');
  48. style.appendChild(document.createTextNode(styleString));
  49. document.body.appendChild(style);
  50. const playerWidth = parseFloat(this.$canvas.css('width')) | this.GRAPHDEFWIDTH;
  51. $commentgraph.height(this.GRAPHHEIGHT);
  52. $commentgraph.width(playerWidth);
  53. $commentgraph.css({
  54. background: 'repeating-linear-gradient(to top, #000, #111 5px)',
  55. border: '1px solid #000',
  56. borderTo: 0,
  57. float: 'left',
  58. fontSize: 0,
  59. whiteSpace: 'nowrap',
  60. });
  61. $commentlist.css({
  62. background: '#000',
  63. color: '#fff',
  64. fontSize: '12px',
  65. lineHeight: 1.25,
  66. padding: '4px 4px 0',
  67. pointerEvents: 'none',
  68. position: 'absolute',
  69. zIndex: 9999,
  70. });
  71. }
  72.  
  73.  
  74. drowgraph(commentData, $canvas) {
  75. const $commentgraph = this.$commentgraph;
  76. const $commentlist = this.$commentlist;
  77. const ApiJsonData = JSON.parse(document.getElementById('js-initial-watch-data').getAttribute('data-api-data'))
  78. const playerWidth = $commentgraph.width();
  79. const videoTotalTime = ApiJsonData.video.duration;
  80. let barTimeInterval;
  81.  
  82. //TODO 非常に長い(2,3時間以上)動画の処理
  83. //長い動画
  84. if (videoTotalTime > this.MINIMUMBARNUM * this.DEFAULTINTERBAL) {
  85. barTimeInterval = this.DEFAULTINTERBAL;
  86. this.barIndexNum = Math.ceil(videoTotalTime / barTimeInterval);
  87. //普通の動画
  88. } else if (videoTotalTime > this.MINIMUMBARNUM) {
  89. this.barIndexNum = this.MINIMUMBARNUM;
  90. barTimeInterval = videoTotalTime / this.MINIMUMBARNUM;
  91. } else {
  92. //MINIMUMBARNUM秒以下の短い動画
  93. this.barIndexNum = Math.floor(videoTotalTime);
  94. barTimeInterval = 1;
  95. }
  96.  
  97. $commentgraph.width(playerWidth);
  98. const barColors = [
  99. '003165', '00458f', '0058b5', '005fc4', '006adb',
  100. '0072ec', '007cff', '55a7ff', '3d9bff'
  101. ];
  102. let listCounts = (new Array(this.barIndexNum + 1)).fill(0);
  103. const listMessages = (new Array(this.barIndexNum + 1)).fill("");
  104. const listTimes = (new Array(this.barIndexNum + 1)).fill("");
  105. const lastBarTimeIntervalGap = Math.floor(videoTotalTime - (this.barIndexNum * barTimeInterval));
  106. const barWidth = playerWidth / this.barIndexNum;
  107.  
  108. const MAXCOMMENTNUM = this.MAXCOMMENTNUM;
  109.  
  110. for (let item of commentData) {
  111. if (item['chat'] === undefined) {
  112. console.dir(item)
  113. continue;
  114. }
  115. let vpos = item.chat.vpos / 100;
  116. //動画長を超えた時間のpostがあるため対処
  117. if (videoTotalTime <= vpos) {
  118. vpos = videoTotalTime;
  119. }
  120. const section = Math.floor(vpos / barTimeInterval);
  121. listCounts[section]++;
  122. if (listCounts[section] <= MAXCOMMENTNUM) {
  123. const comment = item.chat.content.replace(/"|<|&lt;/g, ' ').replace(/\n/g, '<br>');
  124. listMessages[section] += comment + '<br>';
  125. }
  126. }
  127.  
  128.  
  129. let starttime = 0;
  130. let nexttime = 0;
  131. for (let i = 0; i < this.barIndexNum; i++) {
  132. starttime = nexttime;
  133. nexttime += barTimeInterval;
  134. if (i == this.barIndexNum - 1) {
  135. nexttime += lastBarTimeIntervalGap;
  136. }
  137. const startmin = Math.floor(starttime / 60);
  138. const startsec = Math.floor(starttime - startmin * 60);
  139. let endmin = Math.floor(nexttime / 60);
  140. let endsec = Math.ceil(nexttime - endmin * 60);
  141. if (59 < endsec) {
  142. endmin += 1;
  143. endsec -= 60;
  144. }
  145. listTimes[i] += `${("0" + startmin).slice(-2)}:${("0" + startsec).slice(-2)}-${("0" + endmin).slice(-2)}:${("0" + endsec).slice(-2)}`;
  146. }
  147.  
  148. // TODO なぜかthis.barIndexNum以上の配列ができる
  149. listCounts = listCounts.slice(0, this.barIndexNum);
  150. const listCountMax = Math.max.apply(null, listCounts);
  151. const barColorRatio = (barColors.length - 1) / listCountMax;
  152.  
  153. $commentgraph.empty();
  154. $commentgraph.height(this.GRAPHHEIGHT);
  155.  
  156. for (let i = 0; i < this.barIndexNum; i++) {
  157. const barColor = barColors[Math.floor(listCounts[i] * barColorRatio)];
  158. const barBackground = `linear-gradient(to top, #${barColor}, #${barColor} ` +
  159. `${listCounts[i]}px, transparent ${listCounts[i]}px, transparent)`;
  160. const barText = listCounts[i] ?
  161. `${listMessages[i]}<br><br>${listTimes[i]} コメ ${listCounts[i]}` : '';
  162. $('<div>')
  163. .css('background-image', barBackground)
  164. .css('float', 'left')
  165. .data('text', barText)
  166. .height(this.GRAPHHEIGHT)
  167. .width(barWidth)
  168. .addClass("commentbar")
  169. .appendTo($commentgraph);
  170. }
  171. }
  172.  
  173. addMousefunc($canvas) {
  174. const $commentgraph = this.$commentgraph;
  175. const $commentlist = this.$commentlist;
  176.  
  177. function mouseOverFunc() {
  178. $commentlist.css({
  179. 'left': $(this).offset().left,
  180. 'top': $commentgraph.offset().top - $commentlist.height() - 10
  181. }).html($(this).data('text'));
  182. }
  183.  
  184. function mouseOutFunc() {
  185. $commentlist.empty();
  186. }
  187.  
  188. $commentgraph.children().on({
  189. 'mouseenter': function (val) {
  190. $commentlist.css({
  191. 'left': $(this).offset().left,
  192. 'top': $commentgraph.offset().top - $commentlist.height() - 10
  193. }).html($(this).data('text'));
  194. },
  195. 'mousemove': function (val) {
  196. $commentlist.offset({
  197. 'left': $(this).offset().left,
  198. 'top': $commentgraph.offset().top - $commentlist.height() - 10
  199. });
  200. },
  201. 'mouseleave': function () {
  202. $commentlist.empty();
  203. }
  204. });
  205.  
  206. /* 1 Dom Style Watcher本体 監視する側*/
  207. const domStyleWatcher = {
  208. Start: function (tgt, styleobj) {
  209. function eventHappen(data1, data2) {
  210. const throwval = tgt.css(styleobj);
  211. tgt.trigger('domStyleChange', [throwval]);
  212. }
  213.  
  214. const filter = ['style'];
  215. const options = {
  216. attributes: true,
  217. attributeFilter: filter
  218. };
  219. const mutOb = new MutationObserver(eventHappen);
  220. mutOb.observe(tgt, options);
  221. return mutOb;
  222. },
  223. Stop: function (mo) {
  224. mo.disconnect();
  225. }
  226. };
  227.  
  228. function catchEvent(event, value) {
  229. const playerWidth = parseFloat(value);
  230. const barIndexNum = $('.commentbar').length;
  231. $commentgraph.width(playerWidth);
  232. $('.commentbar').width(playerWidth / barIndexNum);
  233. }
  234.  
  235. const target = document.getElementsByClassName('CommentRenderer')[0];
  236. if (target) {
  237. target.addEventListener('domStyleChange', catchEvent);//イベントを登録
  238. domStyleWatcher.Start(target, 'width');//監視開始
  239. }
  240.  
  241. //domStyleWatcher.Stop(dsw);//監視終了
  242. }
  243.  
  244. async getCommentData() {
  245. const ApiJsonData = await JSON.parse(document.getElementById('js-initial-watch-data').getAttribute('data-api-data'));
  246. if (!ApiJsonData) {
  247. return;
  248. }
  249. const threads = ApiJsonData.comment.threads;
  250. const normalCommentId = threads.findIndex(c => c.label === 'default');
  251.  
  252. const server = threads[normalCommentId]["server"];
  253. const threadId = threads[normalCommentId]["id"];
  254.  
  255. const url = `${server}/api.json/thread?thread=${threadId}&version=20090904&res_from=-1000&scores=1`
  256. const params = {
  257. mode: 'cors',
  258. };
  259. const data = await fetch(url, params)
  260. .then(response => response.text())
  261. return JSON.parse(data);
  262. }
  263.  
  264. load() {
  265. const self = this;
  266. this.getCommentData().then(data => {
  267. this.canvas = $('.CommentRenderer').eq(0);
  268. self.drowgraph(data, this.canvas)
  269. self.addMousefunc(this.canvas)
  270. }
  271. )//.catch(console.log("load failed"))
  272. }
  273.  
  274. reload() {
  275. this.load()
  276. }
  277. }
  278.  
  279. // Main
  280. const heatgraph = new NicoHeatGraph();
  281. heatgraph.drawCoordinate();
  282.  
  283. window.onload = () => {
  284. heatgraph.load();
  285. //reload when start button pushed
  286. const startButtons = document.getElementsByClassName('VideoStartButtonContainer')
  287. for (let startbutton of startButtons) {
  288. startbutton.addEventListener('click', () => {
  289. console.log("comment reload.")
  290. heatgraph.reload()
  291. }, false)
  292. }
  293.  
  294. // reload when reload button pushed
  295. const reloadButtons = document.getElementsByClassName('ReloadButton')
  296. for (let reloadButton of reloadButtons) {
  297. reloadButton.addEventListener('click', () => {
  298. console.log("comment reload.")
  299. heatgraph.reload()
  300. }, false)
  301. }
  302. const links = document.getElementsByTagName('a');
  303. for (const link of links) {
  304. link.addEventListener('click', () => {
  305. heatgraph.reload()
  306. });
  307. }
  308.  
  309. }
  310. })();