Nico HeatMap

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

当前为 2014-08-06 提交的版本,查看 最新版本

  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.1.3
  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.WatchApp || !window.WatchJsApi) {
  21. return;
  22. }
  23.  
  24. var $ = window.jQuery, WatchApp = window.WatchApp;
  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. this._commentPanelViewController = WatchApp.ns.init.PlayerInitializer.commentPanelViewController;
  231. },
  232. getComments: function() {
  233. var comments = [];
  234. var commentPanelViewController = this._commentPanelViewController;
  235. var activeListName = commentPanelViewController.commentListModel.getListName();
  236. var list = commentPanelViewController.commentLists;
  237.  
  238. for (var i = 0; i < list.length; i++) {
  239. if (list[i].listName === activeListName) {
  240. comments = list[i].comments;
  241. break;
  242. }
  243. var ct = list[i].comments;
  244. comments = (comments.length < ct.length) ? ct : comments;
  245. }
  246. return comments;
  247. }
  248. };
  249.  
  250. var HeatMapModel = function() { this.initialize.apply(this, arguments); };
  251. HeatMapModel.prototype = {
  252. _nicoplayer: null,
  253. _WatchApp: null,
  254. _resolution: 100,
  255. initialize: function(params) {
  256. this._view = params.view;
  257. this._nicoplayer = params.nicoplayer;
  258. this._resolution = params.resolution || 100;
  259. this._WatchApp = params.WatchApp;
  260. this._commentList = new CommentList(this._WatchApp);
  261. },
  262. update: function() {
  263. var map = this._getHeatMap(this._commentList.getComments(), this._getDuration());
  264. this._view.update(map);
  265. },
  266. reset: function() {
  267. this._view.reset();
  268. },
  269. _getDuration: function() {
  270. return this._nicoplayer.ext_getTotalTime(); // watchInfoModelよりたぶん正確
  271. },
  272. _getHeatMap: function(comments, duration) {
  273. var map = new Array(Math.max(Math.min(this._resolution, Math.floor(duration)), 1)), i = map.length; while(i > 0) map[--i] = 0;
  274. var ratio = duration > map.length ? (map.length / duration) : 1;
  275.  
  276. for (i = comments.length - 1; i >= 0; i--) {
  277. var pos = comments[i].vpos , mpos = Math.min(Math.floor(pos * ratio / 1000), map.length -1);
  278. map[mpos]++;
  279. }
  280.  
  281. return map;
  282. }
  283. };
  284.  
  285. var HeatMapView = function() { this.initialize.apply(this, arguments); };
  286. HeatMapView.prototype = {
  287. _canvas: null,
  288. _context: null,
  289. _palette: null,
  290. _width: 100,
  291. _height: 12,
  292. initialize: function(params) {
  293. this._width = params.width;
  294. this._height = params.height;
  295.  
  296. this._initializePalette();
  297. this._initializeCanvas(params);
  298. },
  299. _initializePalette: function() {
  300. this._palette = [];
  301. for (var c = 0; c < 256; c++) {
  302. var
  303. r = Math.floor((c > 127) ? (c / 2 + 128) : 0),
  304. g = Math.floor((c > 127) ? (255 - (c - 128) * 2) : (c * 2)),
  305. b = Math.floor((c > 127) ? 0 : (255 - c * 2));
  306. this._palette.push('rgb(' + r + ', ' + g + ', ' + b + ')');
  307. }
  308. },
  309. _initializeCanvas: function(params) {
  310. var $container = $('<div id="nicoHeatMapContainer" />');
  311. $container.on('dblclick', function(e) { // ダブルクリックしたら固定表示にする(オマケ)
  312. e.preventDefault();
  313. e.stopPropagation();
  314. $(this).toggleClass('displayAlways');
  315. });
  316.  
  317. if (config.get('heatMapDisplayMode') === 'always') {
  318. $container.addClass('displayAlways');
  319. }
  320. if (config.get('heatMapPosition') === 'bottom') {
  321. $container.addClass('playerBottom');
  322. }
  323.  
  324. this._canvas = document.createElement('canvas');
  325. this._canvas.id = 'nicoHeatMap';
  326. this._canvas.width = this._width;
  327. this._canvas.height = this._height;
  328. $container.append(this._canvas);
  329. $(params.target).append($container);
  330.  
  331. this._context = this._canvas.getContext('2d');
  332.  
  333. this.reset();
  334. },
  335. reset: function() {
  336. this._context.fillStyle = this._palette[0];
  337. this._context.beginPath();
  338. this._context.fillRect(0, 0, this._width, this._height);
  339. },
  340. update: function(map) {
  341.  
  342. // 一番コメント密度が高い所を100%として相対的な比率にする
  343. // 赤い所が常にピークになってわかりやすいが、
  344. // コメントが一カ所に密集している場合はそれ以外が薄くなってしまうのが欠点
  345. var max = 0, i;
  346. // -4 してるのは、末尾にコメントがやたら集中してる事があるのを集計対象外にするため (ニコニ広告に付いてたコメントの名残?)
  347. for (i = Math.max(map.length - 4, 0); i >= 0; i--) max = Math.max(map[i], max);
  348.  
  349. if (max > 0) {
  350. var rate = 255 / max;
  351. for (i = map.length - 1; i >= 0; i--) {
  352. map[i] = Math.min(255, Math.floor(map[i] * rate));
  353. }
  354. } else {
  355. return;
  356. }
  357.  
  358. var
  359. scale = map.length >= this._width ? 1 : (this._width / Math.max(map.length, 1)),
  360. blockWidth = (this._width / map.length) * scale,
  361. context = this._context;
  362.  
  363. for (i = map.length - 1; i >= 0; i--) {
  364. context.fillStyle = this._palette[parseInt(map[i], 10)] || this._palette[0];
  365. context.beginPath();
  366. context.fillRect(i * scale, 0, blockWidth, this._height);
  367. }
  368. }
  369. };
  370.  
  371. var HeatMapController = function() { this.initialize.apply(this, arguments); };
  372. HeatMapController.prototype = {
  373. _commentReady: false,
  374. _videoready: false,
  375. _updated: false,
  376. _model: null,
  377. _view: null,
  378. _nicoplayer: null,
  379. initialize: function(params) {
  380. var
  381. $ = params.$, window = params.window,
  382. pac = params.WatchApp.ns.init.PlayerInitializer.playerAreaConnector,
  383. onCommentListInitialized = function() {
  384. window.setTimeout($.proxy(function() {
  385. this._commentReady = true;
  386. this.update();
  387. }, this), 1000);
  388. },
  389. onVideoInitialized = function() {
  390. if (!this._nicoplayer) {
  391. this._nicoplayer = document.getElementById(params.playerId);
  392. this._initializeHeatMap(params);
  393. }
  394. this._videoReady = true;
  395. this.update();
  396. };
  397.  
  398. pac.addEventListener('onCommentListInitialized', $.proxy(onCommentListInitialized, this));
  399. pac.addEventListener('onVideoInitialized', $.proxy(onVideoInitialized , this));
  400. pac.addEventListener('onVideoChangeStatusUpdated', $.proxy(this.reset , this));
  401. },
  402. _initializeHeatMap: function(params) {
  403. this._view = new HeatMapView({
  404. target: params.target,
  405. width: params.width,
  406. height: params.height
  407. });
  408. this._model = new HeatMapModel({
  409. view: this._view,
  410. nicoplayer: this._nicoplayer,
  411. resolution: params.width,
  412. WatchApp: params.WatchApp
  413. });
  414. },
  415. update: function() {
  416. if (!this._commentReady || !this._videoReady || this._updated) return;
  417. try {
  418. console.time('update HeatMap');
  419. this._updated = true;
  420. this._model.update();
  421. console.timeEnd('update HeatMap');
  422. } catch(e) {
  423. console.log('%cException: ', 'color: white; background: red;', e);
  424. console.trace();
  425. }
  426. },
  427. reset: function() {
  428. this._model.reset();
  429. this._commentReady = this._videoReady = this._updated = false;
  430. }
  431. };
  432.  
  433. var initialize = function() {
  434. console.log('%cinitialize NicoHeatMap', 'background: lightgreen;');
  435. window.NicoHeatMap = new HeatMapController({
  436. WatchApp: WatchApp,
  437. resolution: 100,
  438. width: 100,
  439. height: 12,
  440. target: '#nicoplayerContainerInner',
  441. playerId: 'external_nicoplayer',
  442. $: $,
  443. window: window
  444. });
  445. };
  446.  
  447. if (window.PlayerApp) {
  448. (function() {
  449. var watchInfoModel = WatchApp.ns.model.WatchInfoModel.getInstance();
  450. if (watchInfoModel.initialized) {
  451. initialize();
  452. } else {
  453. var onReset = function() {
  454. watchInfoModel.removeEventListener('reset', onReset);
  455. window.setTimeout(function() {
  456. initialize();
  457. }, 0);
  458. };
  459. watchInfoModel.addEventListener('reset', onReset);
  460. }
  461. })();
  462. }
  463.  
  464.  
  465.  
  466. }); // end of monkey
  467.  
  468. var gm = document.createElement('script');
  469. gm.id = 'nicoHeatMapScript';
  470. gm.setAttribute("type", "text/javascript");
  471. gm.setAttribute("charset", "UTF-8");
  472. gm.appendChild(document.createTextNode("(" + monkey + ")(window)"));
  473. document.body.appendChild(gm);
  474.  
  475. })();