Nico HeatMap

コメントの盛り上がり状態をシンプルにグラフ表示。 GINZA用

  1. // ==UserScript==
  2. // @name Nico HeatMap
  3. // @namespace https://github.com/segabito/
  4. // @description コメントの盛り上がり状態をシンプルにグラフ表示。 GINZA用
  5. // @include http://www.nicovideo.jp/watch/*
  6. // @version 1.2.2
  7. // @grant none
  8. // ==/UserScript==
  9.  
  10.  
  11. // ver1.0.2
  12. // GINZAでプレーヤーのサイズが微妙に変わったのに合わせた
  13.  
  14. // TODO: 他にもなんか直そうと思ってたけど思い出せない。思い出したらやる
  15.  
  16. (function() {
  17. var monkey =
  18. (function() {
  19. 'use strict';
  20. if (!window.WatchJsApi) {
  21. return;
  22. }
  23.  
  24. var $ = window.jQuery, require = window.require;
  25.  
  26. var config = (function() {
  27. var prefix = 'NicoHeatMap_';
  28. var conf = {
  29. heatMapPosition: 'default',
  30. heatMapDisplayMode: 'hover'
  31. };
  32. return {
  33. get: function(key) {
  34. try {
  35. if (window.localStorage.hasOwnProperty(prefix + key)) {
  36. return JSON.parse(window.localStorage.getItem(prefix + key));
  37. }
  38. return conf[key];
  39. } catch (e) {
  40. return conf[key];
  41. }
  42. },
  43. set: function(key, value) {
  44. window.localStorage.setItem(prefix + key, JSON.stringify(value));
  45. }
  46. };
  47. })();
  48.  
  49. var $settingPanel = (function(config) {
  50. var $menu = $('<li class="nicoHeatMapSettingMenu"><a href="javascript:;" title="NicoHeatMapの設定変更">NicoHeatMap設定</a></li>');
  51. var $panel = $('<div id="nicoHeatMapSettingPanel" />');//.addClass('open');
  52. // var $button = $('<button class="toggleSetting playerBottomButton">設定</botton>');
  53.  
  54. // $button.on('click', function(e) {
  55. // e.stopPropagation(); e.preventDefault();
  56. // $panel.toggleClass('open');
  57. // });
  58.  
  59. $menu.find('a').on('click', function() { $panel.toggleClass('open'); });
  60.  
  61. var __tpl__ = (function() {/*
  62. <div class="panelHeader">
  63. <h1 class="windowTitle">NicoHeatMapの設定</h1>
  64. <p>設定はリロード後に反映されます</p>
  65. <button class="close" title="閉じる">×</button>
  66. </div>
  67. <div class="panelInner">
  68. <div class="item" data-setting-name="heatMapDisplayMode" data-menu-type="radio">
  69. <h3 class="itemTitle">HeatMapの表示</h3>
  70. <label><input type="radio" value="&quot;always&quot;">常時表示</label>
  71. <label><input type="radio" value="&quot;hover&quot;">ホバー時のみ</label>
  72. </div>
  73.  
  74. <div class="item" data-setting-name="heatMapPosition" data-menu-type="radio">
  75. <h3 class="itemTitle">HeatMapの位置</h3>
  76. <label><input type="radio" value="&quot;bottom&quot;">プレイヤー下</label>
  77. <label><input type="radio" value="&quot;default&quot;">標準</label>
  78. </div>
  79.  
  80. </div>
  81. */}).toString().match(/[^]*\/\*([^]*)\*\/\}$/)[1].replace(/\{\*/g, '/*').replace(/\*\}/g, '*/');
  82. $panel.html(__tpl__);
  83. $panel.find('.item').on('click', function(e) {
  84. var $this = $(this);
  85. var settingName = $this.attr('data-setting-name');
  86. var value = JSON.parse($this.find('input:checked').val());
  87. console.log('seting-name', settingName, 'value', value);
  88. config.set(settingName, value);
  89. }).each(function(e) {
  90. var $this = $(this);
  91. var settingName = $this.attr('data-setting-name');
  92. var value = config.get(settingName);
  93. $this.addClass(settingName);
  94. $this.find('input').attr('name', settingName).val([JSON.stringify(value)]);
  95. });
  96. $panel.find('.close').click(function() {
  97. $panel.removeClass('open');
  98. });
  99.  
  100.  
  101. // $('#playerAlignmentArea').append($button);
  102. $('#siteHeaderRightMenuFix').after($menu);
  103. $('body').append($panel);
  104.  
  105. return $panel;
  106. })(config);
  107.  
  108. var addStyle = function(styles, id) {
  109. var elm = document.createElement('style');
  110. elm.type = 'text/css';
  111. if (id) { elm.id = id; }
  112.  
  113. var text = styles.toString();
  114. text = document.createTextNode(text);
  115. elm.appendChild(text);
  116. var head = document.getElementsByTagName('head');
  117. head = head[0];
  118. head.appendChild(elm);
  119. return elm;
  120. };
  121.  
  122. var __css__ = (function() {/*
  123. #nicoHeatMapContainer {
  124. position: absolute; z-index: 200;
  125. {*bottom: 0px;*} left: 0;
  126. width: 672px;
  127. background: #000; height: 6px;
  128. overflow: hidden;
  129. display: none;
  130. }
  131. .size_normal #nicoHeatMapContainer {
  132. width: 898px;
  133. }
  134. {*.oldTypeCommentInput*} #nicoHeatMapContainer {
  135. bottom: 29px;
  136. }
  137. #content:hover #nicoHeatMapContainer, #nicoHeatMapContainer.displayAlways {
  138. display: block;
  139. }
  140. #nicoHeatMapContainer.displayAlways {
  141. cursor: pointer;
  142. }
  143. #nicoHeatMapContainer.playerBottom {
  144. bottom: -6px;
  145. }
  146.  
  147. {* 全画面・小画面・検索画面では非表示 *}
  148. body.full_with_browser #content #nicoHeatMapContainer,
  149. body.size_small #content #nicoHeatMapContainer,
  150. body.videoSelection #content #nicoHeatMapContainer
  151. {
  152. display: none;
  153. }
  154.  
  155. #nicoHeatMap {
  156. position: absolute; top: 0; left: 0;
  157. transform-origin: 0 0 0;-webkit-transform-origin: 0 0 0;
  158. transform: scaleX(6.72);-webkit-transform: scaleX(6.72);
  159. }
  160.  
  161. .size_normal #nicoHeatMap {
  162. transform: scaleX(8.98); -webkit-transform: scaleX(8.98);
  163. }
  164.  
  165. .nicoHeatMapSettingMenu a {
  166. font-weight: bolder;
  167. white-space: nowrap;
  168. }
  169. #nicoHeatMapSettingPanel {
  170. position: fixed;
  171. bottom: 2000px; right: 8px;
  172. z-index: -1;
  173. width: 500px;
  174. background: #f0f0f0; border: 1px solid black;
  175. padding: 8px;
  176. transition: bottom 0.4s ease-out;
  177. }
  178. #nicoHeatMapSettingPanel.open {
  179. display: block;
  180. bottom: 8px;
  181. box-shadow: 0 0 8px black;
  182. z-index: 10000;
  183. }
  184. #nicoHeatMapSettingPanel .close {
  185. position: absolute;
  186. cursor: pointer;
  187. right: 8px; top: 8px;
  188. }
  189. #nicoHeatMapSettingPanel .panelInner {
  190. background: #fff;
  191. border: 1px inset;
  192. padding: 8px;
  193. min-height: 300px;
  194. overflow-y: scroll;
  195. max-height: 500px;
  196. }
  197. #nicoHeatMapSettingPanel .panelInner .item {
  198. border-bottom: 1px dotted #888;
  199. margin-bottom: 8px;
  200. padding-bottom: 8px;
  201. }
  202. #nicoHeatMapSettingPanel .panelInner .item:hover {
  203. background: #eef;
  204. }
  205. #nicoHeatMapSettingPanel .windowTitle {
  206. font-size: 150%;
  207. }
  208. #nicoHeatMapSettingPanel .itemTitle {
  209. }
  210. #nicoHeatMapSettingPanel label {
  211.  
  212. }
  213. #nicoHeatMapSettingPanel small {
  214. color: #666;
  215. }
  216. #nicoHeatMapSettingPanel .expert {
  217. margin: 32px 0 16px;
  218. font-size: 150%;
  219. background: #ccc;
  220. }
  221.  
  222. */}).toString().match(/[^]*\/\*([^]*)\*\/\}$/)[1].replace(/\{\*/g, '/*').replace(/\*\}/g, '*/');
  223. addStyle(__css__, 'nicoHeatMapCSS');
  224.  
  225.  
  226. var CommentList = function() { this.initialize.apply(this, arguments); };
  227. CommentList.prototype = {
  228. initialize: function(WatchApp) {
  229. this._WatchApp = WatchApp;
  230. var pi = require('watchapp/init/PlayerInitializer');
  231. this._rightSidePanelViewController = pi.rightSidePanelViewController;
  232. },
  233. getComments: function() {
  234. var pt = this._rightSidePanelViewController.getPlayerPanelTabsView();
  235. var cv = pt._commentPanelView;
  236. return cv.getComments().getData();
  237. }
  238. };
  239.  
  240. var HeatMapModel = function() { this.initialize.apply(this, arguments); };
  241. HeatMapModel.prototype = {
  242. _nicoplayer: null,
  243. _WatchApp: null,
  244. _resolution: 100,
  245. initialize: function(params) {
  246. this._view = params.view;
  247. this._nicoplayer = params.nicoplayer;
  248. this._resolution = params.resolution || 100;
  249. this._WatchApp = params.WatchApp;
  250. this._commentList = new CommentList(this._WatchApp);
  251. },
  252. update: function() {
  253. var map = this._getHeatMap(this._commentList.getComments(), this._getDuration());
  254. this._view.update(map);
  255. },
  256. reset: function() {
  257. this._view.reset();
  258. },
  259. _getDuration: function() {
  260. return this._nicoplayer.ext_getTotalTime(); // watchInfoModelよりたぶん正確
  261. },
  262. _getHeatMap: function(comments, duration) {
  263. var map = new Array(Math.max(Math.min(this._resolution, Math.floor(duration)), 1)), i = map.length; while(i > 0) map[--i] = 0;
  264. var ratio = duration > map.length ? (map.length / duration) : 1;
  265.  
  266. for (i = comments.length - 1; i >= 0; i--) {
  267. var pos = comments[i].vpos , mpos = Math.min(Math.floor(pos * ratio / 1000), map.length -1);
  268. map[mpos]++;
  269. }
  270.  
  271. return map;
  272. }
  273. };
  274.  
  275. var HeatMapView = function() { this.initialize.apply(this, arguments); };
  276. HeatMapView.prototype = {
  277. _canvas: null,
  278. _context: null,
  279. _palette: null,
  280. _width: 100,
  281. _height: 12,
  282. initialize: function(params) {
  283. this._width = params.width;
  284. this._height = params.height;
  285.  
  286. this._initializePalette();
  287. this._initializeCanvas(params);
  288. },
  289. _initializePalette: function() {
  290. this._palette = [];
  291. for (var c = 0; c < 256; c++) {
  292. var
  293. r = Math.floor((c > 127) ? (c / 2 + 128) : 0),
  294. g = Math.floor((c > 127) ? (255 - (c - 128) * 2) : (c * 2)),
  295. b = Math.floor((c > 127) ? 0 : (255 - c * 2));
  296. this._palette.push('rgb(' + r + ', ' + g + ', ' + b + ')');
  297. }
  298. },
  299. _initializeCanvas: function(params) {
  300. var $container = $('<div id="nicoHeatMapContainer" />');
  301. $container.on('dblclick', function(e) { // ダブルクリックしたら固定表示にする(オマケ)
  302. e.preventDefault();
  303. e.stopPropagation();
  304. $(this).toggleClass('displayAlways');
  305. });
  306.  
  307. if (config.get('heatMapDisplayMode') === 'always') {
  308. $container.addClass('displayAlways');
  309. }
  310. if (config.get('heatMapPosition') === 'bottom') {
  311. $container.addClass('playerBottom');
  312. }
  313.  
  314. this._canvas = document.createElement('canvas');
  315. this._canvas.id = 'nicoHeatMap';
  316. this._canvas.width = this._width;
  317. this._canvas.height = this._height;
  318. $container.append(this._canvas);
  319. $(params.target).append($container);
  320.  
  321. this._context = this._canvas.getContext('2d');
  322.  
  323. this.reset();
  324. },
  325. reset: function() {
  326. this._context.fillStyle = this._palette[0];
  327. this._context.beginPath();
  328. this._context.fillRect(0, 0, this._width, this._height);
  329. },
  330. update: function(map) {
  331.  
  332. // 一番コメント密度が高い所を100%として相対的な比率にする
  333. // 赤い所が常にピークになってわかりやすいが、
  334. // コメントが一カ所に密集している場合はそれ以外が薄くなってしまうのが欠点
  335. var max = 0, i;
  336. // -4 してるのは、末尾にコメントがやたら集中してる事があるのを集計対象外にするため (ニコニ広告に付いてたコメントの名残?)
  337. for (i = Math.max(map.length - 4, 0); i >= 0; i--) max = Math.max(map[i], max);
  338.  
  339. if (max > 0) {
  340. var rate = 255 / max;
  341. for (i = map.length - 1; i >= 0; i--) {
  342. map[i] = Math.min(255, Math.floor(map[i] * rate));
  343. }
  344. } else {
  345. return;
  346. }
  347.  
  348. var
  349. scale = map.length >= this._width ? 1 : (this._width / Math.max(map.length, 1)),
  350. blockWidth = (this._width / map.length) * scale,
  351. context = this._context;
  352.  
  353. for (i = map.length - 1; i >= 0; i--) {
  354. context.fillStyle = this._palette[parseInt(map[i], 10)] || this._palette[0];
  355. context.beginPath();
  356. context.fillRect(i * scale, 0, blockWidth, this._height);
  357. }
  358. }
  359. };
  360.  
  361. var HeatMapController = function() { this.initialize.apply(this, arguments); };
  362. HeatMapController.prototype = {
  363. _commentReady: false,
  364. _videoready: false,
  365. _updated: false,
  366. _model: null,
  367. _view: null,
  368. _nicoplayer: null,
  369. initialize: function(params) {
  370. var
  371. $ = params.$, window = params.window,
  372. pac = params.PlayerInitializer.playerAreaConnector,
  373. npc = params.NicoPlayerConnector,
  374. onCommentListInitialized = function() {
  375. window.setTimeout($.proxy(function() {
  376. this._commentReady = true;
  377. this.update();
  378. }, this), 1000);
  379. },
  380. onVideoInitialized = function() {
  381. if (!this._nicoplayer) {
  382. this._nicoplayer = document.getElementById(params.playerId);
  383. this._initializeHeatMap(params);
  384. }
  385. this._videoReady = true;
  386. this.update();
  387. };
  388. var advice = require('advice');
  389. advice.after(npc, 'onCommentListInitialized', $.proxy(onCommentListInitialized, this));
  390.  
  391. pac.addEventListener('onVideoInitialized', $.proxy(onVideoInitialized , this));
  392. pac.addEventListener('onVideoChangeStatusUpdated', $.proxy(this.reset , this));
  393. },
  394. _initializeHeatMap: function(params) {
  395. this._view = new HeatMapView({
  396. target: params.target,
  397. width: params.width,
  398. height: params.height
  399. });
  400. this._model = new HeatMapModel({
  401. view: this._view,
  402. nicoplayer: this._nicoplayer,
  403. resolution: params.width,
  404. WatchApp: params.WatchApp
  405. });
  406. },
  407. update: function() {
  408. if (!this._commentReady || !this._videoReady || this._updated) return;
  409. try {
  410. console.time('update HeatMap');
  411. this._updated = true;
  412. this._model.update();
  413. console.timeEnd('update HeatMap');
  414. } catch(e) {
  415. console.log('%cException: ', 'color: white; background: red;', e);
  416. console.trace();
  417. }
  418. },
  419. reset: function() {
  420. this._model.reset();
  421. this._commentReady = this._videoReady = this._updated = false;
  422. }
  423. };
  424.  
  425. var initialize = function() {
  426. console.log('%cinitialize NicoHeatMap', 'background: lightgreen;');
  427. window.NicoHeatMap = new HeatMapController({
  428. WatchApp: require('WatchApp'),
  429. PlayerInitializer: require('watchapp/init/PlayerInitializer'),
  430. NicoPlayerConnector: require('watchapp/model/player/NicoPlayerConnector'),
  431. resolution: 100,
  432. width: 100,
  433. height: 12,
  434. target: '#nicoplayerContainerInner',
  435. playerId: 'external_nicoplayer',
  436. $: $,
  437. window: window
  438. });
  439. console.log('%cinitialize NicoHeatMap OK', 'background: lightgreen;');
  440. };
  441.  
  442. if (window.WatchJsApi) {
  443. require(['watchapp/model/WatchInfoModel'], function(WatchInfoModel) {
  444. var watchInfoModel = WatchInfoModel.getInstance();
  445. if (watchInfoModel.initialized) {
  446. initialize();
  447. } else {
  448. var onReset = function() {
  449. watchInfoModel.removeEventListener('reset', onReset);
  450. window.setTimeout(function() {
  451. initialize();
  452. }, 0);
  453. };
  454. watchInfoModel.addEventListener('reset', onReset);
  455. }
  456. });
  457. }
  458.  
  459.  
  460.  
  461. }); // end of monkey
  462.  
  463. var gm = document.createElement('script');
  464. gm.id = 'nicoHeatMapScript';
  465. gm.setAttribute("type", "text/javascript");
  466. gm.setAttribute("charset", "UTF-8");
  467. if (location.pathname.indexOf('/watch/') === 0) {
  468. gm.appendChild(document.createTextNode(
  469. 'require(["WatchApp", "jquery", "lodash"], function() {' +
  470. 'console.log("%crequire WatchApp", "background: lightgreen;");' +
  471. '(' + monkey + ')();' +
  472. '});'
  473. ));
  474. } else {
  475. gm.appendChild(document.createTextNode('(' + monkey + ')();'));
  476. }
  477. document.body.appendChild(gm);
  478.  
  479. })();