HistogramHeatGraph_html5.user.js

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

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
  1. // ==UserScript==
  2. // @name HistogramHeatGraph_html5.user.js
  3. // @namespace sotoba
  4. // @version 1.1.9.20210816
  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 || item.chat.content === undefined) {
  112. continue;
  113. }
  114. let vpos = item.chat.vpos / 100;
  115. //動画長を超えた時間のpostがあるため対処
  116. if (videoTotalTime <= vpos) {
  117. vpos = videoTotalTime;
  118. }
  119. const section = Math.floor(vpos / barTimeInterval);
  120. listCounts[section]++;
  121. if (listCounts[section] <= MAXCOMMENTNUM) {
  122. const comment = item.chat.content.replace(/"|<|&lt;/g, ' ').replace(/\n/g, '<br>');
  123. listMessages[section] += comment + '<br>';
  124. }
  125. }
  126.  
  127.  
  128. let starttime = 0;
  129. let nexttime = 0;
  130. for (let i = 0; i < this.barIndexNum; i++) {
  131. starttime = nexttime;
  132. nexttime += barTimeInterval;
  133. if (i == this.barIndexNum - 1) {
  134. nexttime += lastBarTimeIntervalGap;
  135. }
  136. const startmin = Math.floor(starttime / 60);
  137. const startsec = Math.floor(starttime - startmin * 60);
  138. let endmin = Math.floor(nexttime / 60);
  139. let endsec = Math.ceil(nexttime - endmin * 60);
  140. if (59 < endsec) {
  141. endmin += 1;
  142. endsec -= 60;
  143. }
  144. listTimes[i] += `${("0" + startmin).slice(-2)}:${("0" + startsec).slice(-2)}-${("0" + endmin).slice(-2)}:${("0" + endsec).slice(-2)}`;
  145. }
  146.  
  147. // TODO なぜかthis.barIndexNum以上の配列ができる
  148. listCounts = listCounts.slice(0, this.barIndexNum);
  149. const listCountMax = Math.max.apply(null, listCounts);
  150. const barColorRatio = (barColors.length - 1) / listCountMax;
  151.  
  152. $commentgraph.empty();
  153. $commentgraph.height(this.GRAPHHEIGHT);
  154.  
  155. for (let i = 0; i < this.barIndexNum; i++) {
  156. const barColor = barColors[Math.floor(listCounts[i] * barColorRatio)];
  157. const barBackground = `linear-gradient(to top, #${barColor}, #${barColor} ` +
  158. `${listCounts[i]}px, transparent ${listCounts[i]}px, transparent)`;
  159. const barText = listCounts[i] ?
  160. `${listMessages[i]}<br><br>${listTimes[i]} コメ ${listCounts[i]}` : '';
  161. $('<div>')
  162. .css('background-image', barBackground)
  163. .css('float', 'left')
  164. .data('text', barText)
  165. .height(this.GRAPHHEIGHT)
  166. .width(barWidth)
  167. .addClass("commentbar")
  168. .appendTo($commentgraph);
  169. }
  170. }
  171.  
  172. addMousefunc($canvas) {
  173. const $commentgraph = this.$commentgraph;
  174. const $commentlist = this.$commentlist;
  175.  
  176. function mouseOverFunc() {
  177. $commentlist.css({
  178. 'left': $(this).offset().left,
  179. 'top': $commentgraph.offset().top - $commentlist.height() - 10
  180. }).html($(this).data('text'));
  181. }
  182.  
  183. function mouseOutFunc() {
  184. $commentlist.empty();
  185. }
  186.  
  187. $commentgraph.children().on({
  188. 'mouseenter': function (val) {
  189. $commentlist.css({
  190. 'left': $(this).offset().left,
  191. 'top': $commentgraph.offset().top - $commentlist.height() - 10
  192. }).html($(this).data('text'));
  193. },
  194. 'mousemove': function (val) {
  195. $commentlist.offset({
  196. 'left': $(this).offset().left,
  197. 'top': $commentgraph.offset().top - $commentlist.height() - 10
  198. });
  199. },
  200. 'mouseleave': function () {
  201. $commentlist.empty();
  202. }
  203. });
  204.  
  205. /* 1 Dom Style Watcher本体 監視する側*/
  206. const domStyleWatcher = {
  207. Start: function (tgt, styleobj) {
  208. function eventHappen(data1, data2) {
  209. const throwval = tgt.css(styleobj);
  210. tgt.trigger('domStyleChange', [throwval]);
  211. }
  212.  
  213. const filter = ['style'];
  214. const options = {
  215. attributes: true,
  216. attributeFilter: filter
  217. };
  218. const mutOb = new MutationObserver(eventHappen);
  219. mutOb.observe(tgt, options);
  220. return mutOb;
  221. },
  222. Stop: function (mo) {
  223. mo.disconnect();
  224. }
  225. };
  226.  
  227. function catchEvent(event, value) {
  228. const playerWidth = parseFloat(value);
  229. const barIndexNum = $('.commentbar').length;
  230. $commentgraph.width(playerWidth);
  231. $('.commentbar').width(playerWidth / barIndexNum);
  232. }
  233.  
  234. const target = document.getElementsByClassName('CommentRenderer')[0];
  235. if (target) {
  236. target.addEventListener('domStyleChange', catchEvent);//イベントを登録
  237. domStyleWatcher.Start(target, 'width');//監視開始
  238. }
  239.  
  240. //domStyleWatcher.Stop(dsw);//監視終了
  241. }
  242.  
  243. async getCommentData() {
  244. const ApiJsonData = await JSON.parse(document.getElementById('js-initial-watch-data').getAttribute('data-api-data'));
  245. if (!ApiJsonData) {
  246. return;
  247. }
  248. const threads = ApiJsonData.comment.threads;
  249. const normalCommentId = threads.findIndex(c => c.label === 'default');
  250.  
  251. const server = threads[normalCommentId]["server"];
  252. const threadId = threads[normalCommentId]["id"];
  253.  
  254. const url = `${server}/api.json/thread?thread=${threadId}&version=20090904&res_from=-1000&scores=1`
  255. const params = {
  256. mode: 'cors',
  257. };
  258. const data = await fetch(url, params)
  259. .then(response => response.text())
  260. return JSON.parse(data);
  261. }
  262.  
  263. load() {
  264. const self = this;
  265. this.getCommentData().then(data => {
  266. this.canvas = $('.CommentRenderer').eq(0);
  267. self.drowgraph(data, this.canvas)
  268. self.addMousefunc(this.canvas)
  269. }
  270. )//.catch(console.log("load failed"))
  271. }
  272.  
  273. reload() {
  274. this.load()
  275. }
  276. }
  277.  
  278. // Main
  279. const heatgraph = new NicoHeatGraph();
  280. heatgraph.drawCoordinate();
  281.  
  282. window.onload = () => {
  283. heatgraph.load();
  284. //reload when start button pushed
  285. const startButtons = document.getElementsByClassName('VideoStartButtonContainer')
  286. for (let startbutton of startButtons) {
  287. startbutton.addEventListener('click', () => {
  288. console.log("comment reload.")
  289. heatgraph.reload()
  290. }, false)
  291. }
  292.  
  293. // reload when reload button pushed
  294. const reloadButtons = document.getElementsByClassName('ReloadButton')
  295. for (let reloadButton of reloadButtons) {
  296. reloadButton.addEventListener('click', () => {
  297. console.log("comment reload.")
  298. heatgraph.reload()
  299. }, false)
  300. }
  301. const links = document.getElementsByTagName('a');
  302. for (const link of links) {
  303. link.addEventListener('click', () => {
  304. heatgraph.reload()
  305. });
  306. }
  307.  
  308. }
  309. })();