Nico HeatMap

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

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==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);

})();