Nico HeatMap

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

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

You will need to install an extension such as Tampermonkey to install this script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==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="&quot;always&quot;">常時表示</label>
              <label><input type="radio" value="&quot;hover&quot;">ホバー時のみ</label>
            </div>

            <div class="item" data-setting-name="heatMapPosition" data-menu-type="radio">
              <h3 class="itemTitle">HeatMapの位置</h3>
              <label><input type="radio" value="&quot;bottom&quot;">プレイヤー下</label>
              <label><input type="radio" value="&quot;default&quot;">標準</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);

})();