- // ==UserScript==
- // @name Nico HeatMap
- // @namespace https://github.com/segabito/
- // @description コメントの盛り上がり状態をシンプルにグラフ表示。 GINZA用
- // @include http://www.nicovideo.jp/watch/*
- // @version 1.2.2
- // @grant none
- // ==/UserScript==
-
-
- // ver1.0.2
- // GINZAでプレーヤーのサイズが微妙に変わったのに合わせた
-
- // TODO: 他にもなんか直そうと思ってたけど思い出せない。思い出したらやる
-
- (function() {
- var monkey =
- (function() {
- 'use strict';
- if (!window.WatchJsApi) {
- return;
- }
-
- var $ = window.jQuery, require = window.require;
-
- var config = (function() {
- var prefix = 'NicoHeatMap_';
- var conf = {
- heatMapPosition: 'default',
- heatMapDisplayMode: 'hover'
- };
- return {
- get: function(key) {
- try {
- if (window.localStorage.hasOwnProperty(prefix + key)) {
- return JSON.parse(window.localStorage.getItem(prefix + key));
- }
- return conf[key];
- } catch (e) {
- return conf[key];
- }
- },
- set: function(key, value) {
- window.localStorage.setItem(prefix + key, JSON.stringify(value));
- }
- };
- })();
-
- var $settingPanel = (function(config) {
- var $menu = $('<li class="nicoHeatMapSettingMenu"><a href="javascript:;" title="NicoHeatMapの設定変更">NicoHeatMap設定</a></li>');
- var $panel = $('<div id="nicoHeatMapSettingPanel" />');//.addClass('open');
- // var $button = $('<button class="toggleSetting playerBottomButton">設定</botton>');
-
- // $button.on('click', function(e) {
- // e.stopPropagation(); e.preventDefault();
- // $panel.toggleClass('open');
- // });
-
- $menu.find('a').on('click', function() { $panel.toggleClass('open'); });
-
- var __tpl__ = (function() {/*
- <div class="panelHeader">
- <h1 class="windowTitle">NicoHeatMapの設定</h1>
- <p>設定はリロード後に反映されます</p>
- <button class="close" title="閉じる">×</button>
- </div>
- <div class="panelInner">
- <div class="item" data-setting-name="heatMapDisplayMode" data-menu-type="radio">
- <h3 class="itemTitle">HeatMapの表示</h3>
- <label><input type="radio" value=""always"">常時表示</label>
- <label><input type="radio" value=""hover"">ホバー時のみ</label>
- </div>
-
- <div class="item" data-setting-name="heatMapPosition" data-menu-type="radio">
- <h3 class="itemTitle">HeatMapの位置</h3>
- <label><input type="radio" value=""bottom"">プレイヤー下</label>
- <label><input type="radio" value=""default"">標準</label>
- </div>
-
- </div>
- */}).toString().match(/[^]*\/\*([^]*)\*\/\}$/)[1].replace(/\{\*/g, '/*').replace(/\*\}/g, '*/');
- $panel.html(__tpl__);
- $panel.find('.item').on('click', function(e) {
- var $this = $(this);
- var settingName = $this.attr('data-setting-name');
- var value = JSON.parse($this.find('input:checked').val());
- console.log('seting-name', settingName, 'value', value);
- config.set(settingName, value);
- }).each(function(e) {
- var $this = $(this);
- var settingName = $this.attr('data-setting-name');
- var value = config.get(settingName);
- $this.addClass(settingName);
- $this.find('input').attr('name', settingName).val([JSON.stringify(value)]);
- });
- $panel.find('.close').click(function() {
- $panel.removeClass('open');
- });
-
-
- // $('#playerAlignmentArea').append($button);
- $('#siteHeaderRightMenuFix').after($menu);
- $('body').append($panel);
-
- return $panel;
- })(config);
-
- var addStyle = function(styles, id) {
- var elm = document.createElement('style');
- elm.type = 'text/css';
- if (id) { elm.id = id; }
-
- var text = styles.toString();
- text = document.createTextNode(text);
- elm.appendChild(text);
- var head = document.getElementsByTagName('head');
- head = head[0];
- head.appendChild(elm);
- return elm;
- };
-
- var __css__ = (function() {/*
- #nicoHeatMapContainer {
- position: absolute; z-index: 200;
- {*bottom: 0px;*} left: 0;
- width: 672px;
- background: #000; height: 6px;
- overflow: hidden;
- display: none;
- }
- .size_normal #nicoHeatMapContainer {
- width: 898px;
- }
- {*.oldTypeCommentInput*} #nicoHeatMapContainer {
- bottom: 29px;
- }
- #content:hover #nicoHeatMapContainer, #nicoHeatMapContainer.displayAlways {
- display: block;
- }
- #nicoHeatMapContainer.displayAlways {
- cursor: pointer;
- }
- #nicoHeatMapContainer.playerBottom {
- bottom: -6px;
- }
-
- {* 全画面・小画面・検索画面では非表示 *}
- body.full_with_browser #content #nicoHeatMapContainer,
- body.size_small #content #nicoHeatMapContainer,
- body.videoSelection #content #nicoHeatMapContainer
- {
- display: none;
- }
-
- #nicoHeatMap {
- position: absolute; top: 0; left: 0;
- transform-origin: 0 0 0;-webkit-transform-origin: 0 0 0;
- transform: scaleX(6.72);-webkit-transform: scaleX(6.72);
- }
-
- .size_normal #nicoHeatMap {
- transform: scaleX(8.98); -webkit-transform: scaleX(8.98);
- }
-
- .nicoHeatMapSettingMenu a {
- font-weight: bolder;
- white-space: nowrap;
- }
- #nicoHeatMapSettingPanel {
- position: fixed;
- bottom: 2000px; right: 8px;
- z-index: -1;
- width: 500px;
- background: #f0f0f0; border: 1px solid black;
- padding: 8px;
- transition: bottom 0.4s ease-out;
- }
- #nicoHeatMapSettingPanel.open {
- display: block;
- bottom: 8px;
- box-shadow: 0 0 8px black;
- z-index: 10000;
- }
- #nicoHeatMapSettingPanel .close {
- position: absolute;
- cursor: pointer;
- right: 8px; top: 8px;
- }
- #nicoHeatMapSettingPanel .panelInner {
- background: #fff;
- border: 1px inset;
- padding: 8px;
- min-height: 300px;
- overflow-y: scroll;
- max-height: 500px;
- }
- #nicoHeatMapSettingPanel .panelInner .item {
- border-bottom: 1px dotted #888;
- margin-bottom: 8px;
- padding-bottom: 8px;
- }
- #nicoHeatMapSettingPanel .panelInner .item:hover {
- background: #eef;
- }
- #nicoHeatMapSettingPanel .windowTitle {
- font-size: 150%;
- }
- #nicoHeatMapSettingPanel .itemTitle {
- }
- #nicoHeatMapSettingPanel label {
-
- }
- #nicoHeatMapSettingPanel small {
- color: #666;
- }
- #nicoHeatMapSettingPanel .expert {
- margin: 32px 0 16px;
- font-size: 150%;
- background: #ccc;
- }
-
- */}).toString().match(/[^]*\/\*([^]*)\*\/\}$/)[1].replace(/\{\*/g, '/*').replace(/\*\}/g, '*/');
- addStyle(__css__, 'nicoHeatMapCSS');
-
-
- var CommentList = function() { this.initialize.apply(this, arguments); };
- CommentList.prototype = {
- initialize: function(WatchApp) {
- this._WatchApp = WatchApp;
- var pi = require('watchapp/init/PlayerInitializer');
- this._rightSidePanelViewController = pi.rightSidePanelViewController;
- },
- getComments: function() {
- var pt = this._rightSidePanelViewController.getPlayerPanelTabsView();
- var cv = pt._commentPanelView;
- return cv.getComments().getData();
- }
- };
-
- var HeatMapModel = function() { this.initialize.apply(this, arguments); };
- HeatMapModel.prototype = {
- _nicoplayer: null,
- _WatchApp: null,
- _resolution: 100,
- initialize: function(params) {
- this._view = params.view;
- this._nicoplayer = params.nicoplayer;
- this._resolution = params.resolution || 100;
- this._WatchApp = params.WatchApp;
- this._commentList = new CommentList(this._WatchApp);
- },
- update: function() {
- var map = this._getHeatMap(this._commentList.getComments(), this._getDuration());
- this._view.update(map);
- },
- reset: function() {
- this._view.reset();
- },
- _getDuration: function() {
- return this._nicoplayer.ext_getTotalTime(); // watchInfoModelよりたぶん正確
- },
- _getHeatMap: function(comments, duration) {
- var map = new Array(Math.max(Math.min(this._resolution, Math.floor(duration)), 1)), i = map.length; while(i > 0) map[--i] = 0;
- var ratio = duration > map.length ? (map.length / duration) : 1;
-
- for (i = comments.length - 1; i >= 0; i--) {
- var pos = comments[i].vpos , mpos = Math.min(Math.floor(pos * ratio / 1000), map.length -1);
- map[mpos]++;
- }
-
- return map;
- }
- };
-
- var HeatMapView = function() { this.initialize.apply(this, arguments); };
- HeatMapView.prototype = {
- _canvas: null,
- _context: null,
- _palette: null,
- _width: 100,
- _height: 12,
- initialize: function(params) {
- this._width = params.width;
- this._height = params.height;
-
- this._initializePalette();
- this._initializeCanvas(params);
- },
- _initializePalette: function() {
- this._palette = [];
- for (var c = 0; c < 256; c++) {
- var
- r = Math.floor((c > 127) ? (c / 2 + 128) : 0),
- g = Math.floor((c > 127) ? (255 - (c - 128) * 2) : (c * 2)),
- b = Math.floor((c > 127) ? 0 : (255 - c * 2));
- this._palette.push('rgb(' + r + ', ' + g + ', ' + b + ')');
- }
- },
- _initializeCanvas: function(params) {
- var $container = $('<div id="nicoHeatMapContainer" />');
- $container.on('dblclick', function(e) { // ダブルクリックしたら固定表示にする(オマケ)
- e.preventDefault();
- e.stopPropagation();
- $(this).toggleClass('displayAlways');
- });
-
- if (config.get('heatMapDisplayMode') === 'always') {
- $container.addClass('displayAlways');
- }
- if (config.get('heatMapPosition') === 'bottom') {
- $container.addClass('playerBottom');
- }
-
- this._canvas = document.createElement('canvas');
- this._canvas.id = 'nicoHeatMap';
- this._canvas.width = this._width;
- this._canvas.height = this._height;
- $container.append(this._canvas);
- $(params.target).append($container);
-
- this._context = this._canvas.getContext('2d');
-
- this.reset();
- },
- reset: function() {
- this._context.fillStyle = this._palette[0];
- this._context.beginPath();
- this._context.fillRect(0, 0, this._width, this._height);
- },
- update: function(map) {
-
- // 一番コメント密度が高い所を100%として相対的な比率にする
- // 赤い所が常にピークになってわかりやすいが、
- // コメントが一カ所に密集している場合はそれ以外が薄くなってしまうのが欠点
- var max = 0, i;
- // -4 してるのは、末尾にコメントがやたら集中してる事があるのを集計対象外にするため (ニコニ広告に付いてたコメントの名残?)
- for (i = Math.max(map.length - 4, 0); i >= 0; i--) max = Math.max(map[i], max);
-
- if (max > 0) {
- var rate = 255 / max;
- for (i = map.length - 1; i >= 0; i--) {
- map[i] = Math.min(255, Math.floor(map[i] * rate));
- }
- } else {
- return;
- }
-
- var
- scale = map.length >= this._width ? 1 : (this._width / Math.max(map.length, 1)),
- blockWidth = (this._width / map.length) * scale,
- context = this._context;
-
- for (i = map.length - 1; i >= 0; i--) {
- context.fillStyle = this._palette[parseInt(map[i], 10)] || this._palette[0];
- context.beginPath();
- context.fillRect(i * scale, 0, blockWidth, this._height);
- }
- }
- };
-
- var HeatMapController = function() { this.initialize.apply(this, arguments); };
- HeatMapController.prototype = {
- _commentReady: false,
- _videoready: false,
- _updated: false,
- _model: null,
- _view: null,
- _nicoplayer: null,
- initialize: function(params) {
- var
- $ = params.$, window = params.window,
- pac = params.PlayerInitializer.playerAreaConnector,
- npc = params.NicoPlayerConnector,
- onCommentListInitialized = function() {
- window.setTimeout($.proxy(function() {
- this._commentReady = true;
- this.update();
- }, this), 1000);
- },
- onVideoInitialized = function() {
- if (!this._nicoplayer) {
- this._nicoplayer = document.getElementById(params.playerId);
- this._initializeHeatMap(params);
- }
- this._videoReady = true;
- this.update();
- };
- var advice = require('advice');
- advice.after(npc, 'onCommentListInitialized', $.proxy(onCommentListInitialized, this));
-
- pac.addEventListener('onVideoInitialized', $.proxy(onVideoInitialized , this));
- pac.addEventListener('onVideoChangeStatusUpdated', $.proxy(this.reset , this));
- },
- _initializeHeatMap: function(params) {
- this._view = new HeatMapView({
- target: params.target,
- width: params.width,
- height: params.height
- });
- this._model = new HeatMapModel({
- view: this._view,
- nicoplayer: this._nicoplayer,
- resolution: params.width,
- WatchApp: params.WatchApp
- });
- },
- update: function() {
- if (!this._commentReady || !this._videoReady || this._updated) return;
- try {
- console.time('update HeatMap');
- this._updated = true;
- this._model.update();
- console.timeEnd('update HeatMap');
- } catch(e) {
- console.log('%cException: ', 'color: white; background: red;', e);
- console.trace();
- }
- },
- reset: function() {
- this._model.reset();
- this._commentReady = this._videoReady = this._updated = false;
- }
- };
-
- var initialize = function() {
- console.log('%cinitialize NicoHeatMap', 'background: lightgreen;');
- window.NicoHeatMap = new HeatMapController({
- WatchApp: require('WatchApp'),
- PlayerInitializer: require('watchapp/init/PlayerInitializer'),
- NicoPlayerConnector: require('watchapp/model/player/NicoPlayerConnector'),
- resolution: 100,
- width: 100,
- height: 12,
- target: '#nicoplayerContainerInner',
- playerId: 'external_nicoplayer',
- $: $,
- window: window
- });
- console.log('%cinitialize NicoHeatMap OK', 'background: lightgreen;');
- };
-
- if (window.WatchJsApi) {
- require(['watchapp/model/WatchInfoModel'], function(WatchInfoModel) {
- var watchInfoModel = WatchInfoModel.getInstance();
- if (watchInfoModel.initialized) {
- initialize();
- } else {
- var onReset = function() {
- watchInfoModel.removeEventListener('reset', onReset);
- window.setTimeout(function() {
- initialize();
- }, 0);
- };
- watchInfoModel.addEventListener('reset', onReset);
- }
- });
- }
-
-
-
- }); // end of monkey
-
- var gm = document.createElement('script');
- gm.id = 'nicoHeatMapScript';
- gm.setAttribute("type", "text/javascript");
- gm.setAttribute("charset", "UTF-8");
- if (location.pathname.indexOf('/watch/') === 0) {
- gm.appendChild(document.createTextNode(
- 'require(["WatchApp", "jquery", "lodash"], function() {' +
- 'console.log("%crequire WatchApp", "background: lightgreen;");' +
- '(' + monkey + ')();' +
- '});'
- ));
- } else {
- gm.appendChild(document.createTextNode('(' + monkey + ')();'));
- }
- document.body.appendChild(gm);
-
- })();