HistogramHeatGraph_html5.user.js

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

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

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