// ==UserScript==
// @name HistogramHeatGraph_html5.user.js
// @namespace sotoba
// @version 1.1.9.20210816
// @description ニコニコ動画のコメントをグラフで表示(html5版)※コメントをリロードすることでグラフを再描画します
// @homepageURL https://github.com/SotobatoNihu/HistogramHeatGraph_html5
// @match https://www.nicovideo.jp/*
// @match https://www.nicovideo.jp/watch/*
// @require https://code.jquery.com/jquery-3.2.1.min.js
// @grant none
// ==/UserScript==
(function () {
'use strict';
// default settings
class NicoHeatGraph {
constructor() {
this.MINIMUMBARNUM = 50;
this.DEFAULTINTERBAL = 10;
this.MAXCOMMENTNUM = 30;
this.GRAPHHEIGHT = 30;
this.GRAPHDEFWIDTH = 856;
this.barIndexNum = 0;
this.$canvas = null;
this.$commentgraph = $('<div>').attr('id', 'comment-graph');
this.$commentlist = $('<div>').attr('id', 'comment-list');
}
drawCoordinate() {
const $commentgraph = this.$commentgraph;
const $commentlist = this.$commentlist;
if (!($('#comment-graph').length)) {
$('.PlayerContainer').eq(0).append($commentgraph);
$('.MainContainer').eq(0).append($commentlist);
}
this.$canvas = $(".CommentRenderer").eq(0);
const styleString = `
#comment-graph :hover{
-webkit-filter: hue-rotate(180deg);
filter: hue-rotate(180deg);
}
#comment-list:empty {
display: none;
}
`;
const style = document.createElement('style');
style.appendChild(document.createTextNode(styleString));
document.body.appendChild(style);
const playerWidth = parseFloat(this.$canvas.css('width')) | this.GRAPHDEFWIDTH;
$commentgraph.height(this.GRAPHHEIGHT);
$commentgraph.width(playerWidth);
$commentgraph.css({
background: 'repeating-linear-gradient(to top, #000, #111 5px)',
border: '1px solid #000',
borderTo: 0,
float: 'left',
fontSize: 0,
whiteSpace: 'nowrap',
});
$commentlist.css({
background: '#000',
color: '#fff',
fontSize: '12px',
lineHeight: 1.25,
padding: '4px 4px 0',
pointerEvents: 'none',
position: 'absolute',
zIndex: 9999,
});
}
drowgraph(commentData, $canvas) {
const $commentgraph = this.$commentgraph;
const $commentlist = this.$commentlist;
const ApiJsonData = JSON.parse(document.getElementById('js-initial-watch-data').getAttribute('data-api-data'))
const playerWidth = $commentgraph.width();
const videoTotalTime = ApiJsonData.video.duration;
let barTimeInterval;
//TODO 非常に長い(2,3時間以上)動画の処理
//長い動画
if (videoTotalTime > this.MINIMUMBARNUM * this.DEFAULTINTERBAL) {
barTimeInterval = this.DEFAULTINTERBAL;
this.barIndexNum = Math.ceil(videoTotalTime / barTimeInterval);
//普通の動画
} else if (videoTotalTime > this.MINIMUMBARNUM) {
this.barIndexNum = this.MINIMUMBARNUM;
barTimeInterval = videoTotalTime / this.MINIMUMBARNUM;
} else {
//MINIMUMBARNUM秒以下の短い動画
this.barIndexNum = Math.floor(videoTotalTime);
barTimeInterval = 1;
}
$commentgraph.width(playerWidth);
const barColors = [
'003165', '00458f', '0058b5', '005fc4', '006adb',
'0072ec', '007cff', '55a7ff', '3d9bff'
];
let listCounts = (new Array(this.barIndexNum + 1)).fill(0);
const listMessages = (new Array(this.barIndexNum + 1)).fill("");
const listTimes = (new Array(this.barIndexNum + 1)).fill("");
const lastBarTimeIntervalGap = Math.floor(videoTotalTime - (this.barIndexNum * barTimeInterval));
const barWidth = playerWidth / this.barIndexNum;
const MAXCOMMENTNUM = this.MAXCOMMENTNUM;
for (let item of commentData) {
if (item.chat === undefined || item.chat.content === undefined) {
continue;
}
let vpos = item.chat.vpos / 100;
//動画長を超えた時間のpostがあるため対処
if (videoTotalTime <= vpos) {
vpos = videoTotalTime;
}
const section = Math.floor(vpos / barTimeInterval);
listCounts[section]++;
if (listCounts[section] <= MAXCOMMENTNUM) {
const comment = item.chat.content.replace(/"|<|</g, ' ').replace(/\n/g, '<br>');
listMessages[section] += comment + '<br>';
}
}
let starttime = 0;
let nexttime = 0;
for (let i = 0; i < this.barIndexNum; i++) {
starttime = nexttime;
nexttime += barTimeInterval;
if (i == this.barIndexNum - 1) {
nexttime += lastBarTimeIntervalGap;
}
const startmin = Math.floor(starttime / 60);
const startsec = Math.floor(starttime - startmin * 60);
let endmin = Math.floor(nexttime / 60);
let endsec = Math.ceil(nexttime - endmin * 60);
if (59 < endsec) {
endmin += 1;
endsec -= 60;
}
listTimes[i] += `${("0" + startmin).slice(-2)}:${("0" + startsec).slice(-2)}-${("0" + endmin).slice(-2)}:${("0" + endsec).slice(-2)}`;
}
// TODO なぜかthis.barIndexNum以上の配列ができる
listCounts = listCounts.slice(0, this.barIndexNum);
const listCountMax = Math.max.apply(null, listCounts);
const barColorRatio = (barColors.length - 1) / listCountMax;
$commentgraph.empty();
$commentgraph.height(this.GRAPHHEIGHT);
for (let i = 0; i < this.barIndexNum; i++) {
const barColor = barColors[Math.floor(listCounts[i] * barColorRatio)];
const barBackground = `linear-gradient(to top, #${barColor}, #${barColor} ` +
`${listCounts[i]}px, transparent ${listCounts[i]}px, transparent)`;
const barText = listCounts[i] ?
`${listMessages[i]}<br><br>${listTimes[i]} コメ ${listCounts[i]}` : '';
$('<div>')
.css('background-image', barBackground)
.css('float', 'left')
.data('text', barText)
.height(this.GRAPHHEIGHT)
.width(barWidth)
.addClass("commentbar")
.appendTo($commentgraph);
}
}
addMousefunc($canvas) {
const $commentgraph = this.$commentgraph;
const $commentlist = this.$commentlist;
function mouseOverFunc() {
$commentlist.css({
'left': $(this).offset().left,
'top': $commentgraph.offset().top - $commentlist.height() - 10
}).html($(this).data('text'));
}
function mouseOutFunc() {
$commentlist.empty();
}
$commentgraph.children().on({
'mouseenter': function (val) {
$commentlist.css({
'left': $(this).offset().left,
'top': $commentgraph.offset().top - $commentlist.height() - 10
}).html($(this).data('text'));
},
'mousemove': function (val) {
$commentlist.offset({
'left': $(this).offset().left,
'top': $commentgraph.offset().top - $commentlist.height() - 10
});
},
'mouseleave': function () {
$commentlist.empty();
}
});
/* 1 Dom Style Watcher本体 監視する側*/
const domStyleWatcher = {
Start: function (tgt, styleobj) {
function eventHappen(data1, data2) {
const throwval = tgt.css(styleobj);
tgt.trigger('domStyleChange', [throwval]);
}
const filter = ['style'];
const options = {
attributes: true,
attributeFilter: filter
};
const mutOb = new MutationObserver(eventHappen);
mutOb.observe(tgt, options);
return mutOb;
},
Stop: function (mo) {
mo.disconnect();
}
};
function catchEvent(event, value) {
const playerWidth = parseFloat(value);
const barIndexNum = $('.commentbar').length;
$commentgraph.width(playerWidth);
$('.commentbar').width(playerWidth / barIndexNum);
}
const target = document.getElementsByClassName('CommentRenderer')[0];
if (target) {
target.addEventListener('domStyleChange', catchEvent);//イベントを登録
domStyleWatcher.Start(target, 'width');//監視開始
}
//domStyleWatcher.Stop(dsw);//監視終了
}
async getCommentData() {
const ApiJsonData = await JSON.parse(document.getElementById('js-initial-watch-data').getAttribute('data-api-data'));
if (!ApiJsonData) {
return;
}
const threads = ApiJsonData.comment.threads;
const normalCommentId = threads.findIndex(c => c.label === 'default');
const server = threads[normalCommentId]["server"];
const threadId = threads[normalCommentId]["id"];
const url = `${server}/api.json/thread?thread=${threadId}&version=20090904&res_from=-1000&scores=1`
const params = {
mode: 'cors',
};
const data = await fetch(url, params)
.then(response => response.text())
return JSON.parse(data);
}
load() {
const self = this;
this.getCommentData().then(data => {
this.canvas = $('.CommentRenderer').eq(0);
self.drowgraph(data, this.canvas)
self.addMousefunc(this.canvas)
}
)//.catch(console.log("load failed"))
}
reload() {
this.load()
}
}
// Main
const heatgraph = new NicoHeatGraph();
heatgraph.drawCoordinate();
window.onload = () => {
heatgraph.load();
//reload when start button pushed
const startButtons = document.getElementsByClassName('VideoStartButtonContainer')
for (let startbutton of startButtons) {
startbutton.addEventListener('click', () => {
console.log("comment reload.")
heatgraph.reload()
}, false)
}
// reload when reload button pushed
const reloadButtons = document.getElementsByClassName('ReloadButton')
for (let reloadButton of reloadButtons) {
reloadButton.addEventListener('click', () => {
console.log("comment reload.")
heatgraph.reload()
}, false)
}
const links = document.getElementsByTagName('a');
for (const link of links) {
link.addEventListener('click', () => {
heatgraph.reload()
});
}
}
})();