HistogramHeatGraph_html5.user.js

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

As of 2018-10-01. See the latest version.

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