【kick】コメントスクロール

try to take over the world!

// ==UserScript==
// @name         【kick】コメントスクロール
// @namespace    http://tampermonkey.net/
// @version      0.7.2
// @description  try to take over the world!
// @author       You
// @match        https://kick.com*
// @match        https://kick.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=kick.com
// @grant        none
// @license MIT
// ==/UserScript==

//メソッドが同頑張っても呼び出せないバグ発生中のためresetボタンは使えません

const setting = {
    CommentColor : "white",              //コメントの色
    FontSize : 35,                     //コメントのサイズ  おすすめ 30 ~ 45
    FontFamily : "'Roboto', sans-serif", //フォント
    Alpha : 0.7,                         //透過率 0 ~ 1     0.7 → 70%
    StrokeColor : "black",               //縁取りの色
    LineSize : 1,                        //縁取りの幅
    BoostRate : [0.1, 5],                //コメント加速率   [0.1, 5] → 5文字ごとに10%増加
    ScrollSpeed : 3.5,                   //基礎速度
};


///////////////////////////*                         DANGER                            *//////////////////////////////////

const HTML = {
    CanvasHTML : `<div class="absolute left-0 top-0 h-full w-full" style="pointer-events: none;">
    <canvas id="comment_canvas"></canvas>
    </div>`,

    ResetBtnHTML : `<button id="resetBTN">reset</button>`,
}



class CommentScroll {
    constructor(setting, HTML) {
        this.initSettings(setting);    //設定読み込み
        this.initFlg();     //フラグ
        this.initCanvasProperties();
        this.initHTML(HTML);//this.をしても何故かthis.canvasHTML等に入らず、からの状態になっている。HTMLObjectから直接呼び出すと動く
        this.videoElement = '';
        this.lastTime = performance.now();
        this.fps = 60;
        this.interval = 1000 / this.fps;
        this.observer;
    }

    initSettings(setting) {
        this.color = setting["CommentColor"];
        this.commentPX = setting["FontSize"];
        this.fontSize = setting["FontSize"];
        this.fontFamily = setting["FontFamily"];
        this.Alpha = setting["Alpha"];
        this.strokeColor = setting["StrokeColor"];
        this.lineW = setting["LineSize"];
        this.boostRate = setting["BoostRate"];
        this.scrollSpeed = setting["ScrollSpeed"];
    }

    initHTML(HTML){
        this.canvasHTML = HTML["CanvasHTML"];
        this.resetBtnHTML = HTML["ResetBtnHTML"];
    }

    initCanvasProperties(){
        this.canvas = '';
        this.ctx = '';
        this.canvasWid = 800;
        this.commentLane = [50, 100, 150, 200, 250, 300, 350, 400, 450];
        this.laneCnt = 0;
        this.id = 0;
        this.commentObj = {};
    }

    initFlg(){
        this.setup_flg = false;
        this.observer_flg = false;
        this.animationRunning = false;
    }

    setup(){
        this.commentObj = {};
        if (this.setup_flg) return;
        if (document.getElementById('video-player') && location.href !=='https://kick.com/') {
            console.log(85);
            this.addCanvas();
            this.canvasSizeSet(); //キャンバスのサイズをセット
            this.commentget_observer(); //コメント監視開始
            this.canvasAnimation();
            this.videoSkipEvent();
            this.setup_flg = true;
        }
    }

    firstEvent() {
        window.addEventListener('resize', ()=> this.canvasSizeSet());

        window.addEventListener("keydown", (e) => {
            if (e.code === 'KeyT') this.canvasSizeSet();
        })

        setTimeout(()=> this.setup(), 4000); //たまに人間が操作しているかの確認が入るので遅めに設定 (短いと引っかかる)
        setTimeout(()=>this.resetBtn(), 2000);
    }

    resetBtn(){
        if(document.getElementById('resetBTN') === null){
            document.getElementById('channel-chatroom').querySelector('div:first-child > div').insertAdjacentHTML('afterend', this.resetBtnHTML);
            document.getElementById('resetBTN').addEventListener("click", this.reset);
        }
    }

    reset(){
        if (this.observer) {
            this.observer.disconnect();
        };
        // リセット処理
        this.initFlg();
        this.initCanvasProperties();
        //this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
        this.videoElement = '';
        // 再セットアップ
        this.setup();
        this.canvasSizeSet();
    }


    commentget_observer() {
        this.commentObj = {};
        console.log("鬱");
        this.observer = new MutationObserver(records => {
            records.forEach((r) => {
                const rNode = r.addedNodes;
                if (rNode.length != 0) {
                    const commentElement = rNode[0].querySelectorAll('div > div > span')[2]
                    if (!commentElement) return;
                    const com = commentElement.textContent
                    if (com.length != 0 && com != ': ') {
                        //スタンプコメ除外 返信が:だけになってしまうので返信削除
                        this.commentPushToObj(com);
                    }

                }
            })

        })

        const chatDivCheck = () => {
            const channelChatroom = document.getElementById('channel-chatroom');
            const chatDiv = channelChatroom.querySelector('.no-scrollbar');
                if(chatDiv === null){
                    setTimeout(chatDivCheck, 500);
                }else{
                    this.observer.disconnect();
                    this.observer.observe(chatDiv, {
                        childList: true
                    })
                }
        }
        chatDivCheck();
    }

    commentPushToObj(text) {
        this.commentObj[this.id] = {
            TEXT: text,
            w: this.canvasWid + ((Object.keys(this.commentObj).length / this.commentLane.length) * 30) ,//レーン数を込めるコメントが読み込まれた場合ずらす
            s: Math.ceil(this.scrollSpeed * (1 * (1 + this.boostRate[0] * Math.floor(text.length /
                this.boostRate[1])))),
            y: this.laneCnt,
        }

        this.laneCnt = (this.laneCnt >= this.commentLane.length) ? 0 : this.laneCnt + 1;
        this.id++;
    }

    addCanvas() {
        this.videoElement = document.getElementById('video-player');

        this.videoElement.insertAdjacentHTML('afterend', this.canvasHTML);
        const getCanvas = () =>{
            this.canvas = document.getElementById('comment_canvas');
            if(this.canvas == ''){
                setTimeout(getCanvas,200);
            }else{
                console.log(this.canvas);
                this.ctx = this.canvas.getContext('2d');
                this.ctx.font = `${this.fontSize}px ${this.fontFamily}`;
                this.ctx.fillStyle = this.color;
                this.videoElement.parentElement.addEventListener("click", ()=> this.canvasSizeSet());
                return;
            }
        }
        getCanvas();
    }

    canvasSizeSet() {
        setTimeout(() => {
            if(!this.videoElement) return;
            const rect = this.videoElement.getBoundingClientRect();
            this.canvas.width = rect.width;
            this.canvas.height = rect.height;
            this.canvasWid = this.canvas.width;
        }, 1000);
    }

    canvasAnimation() {
        if (this.animationRunning) return; // 既にアニメーションが動いている場合は何もしない
        this.animationRunning = true;
        const animate = (currentTime) => {
            const deltaTime = currentTime - this.lastTime;
            if (deltaTime >= this.interval) {
                this.lastTime = currentTime - (deltaTime % this.interval);
                anime();
            }
            requestAnimationFrame(animate);
        }

        const anime = () => {
            if(!this.ctx) return;
            this.ctx.font = `${this.fontSize}px ${this.fontFamily}`;
            this.ctx.fillStyle = this.color;
            this.ctx.globalAlpha = this.Alpha;
            this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
            this.ctx.lineWidth = this.lineW;
            this.ctx.strokeStyle = this.strokeColor;
            Object.entries(this.commentObj).forEach(([id, c]) => {
                c["w"] -= c["s"]

                if (c["w"] < 0 - this.commentPX * c["TEXT"].length) {
                    delete this.commentObj[id]; // ここでidを使って削除
                    if (Object.keys(this.commentObj).length < 5) {
                        this.laneCnt = 0;
                    }
                } else {
                    this.ctx.strokeText(c["TEXT"], c["w"], this.commentLane[c["y"]]);
                    this.ctx.fillText(c["TEXT"], c["w"], this.commentLane[c["y"]]); // コメを描画
                }
            });
        }
        animate();
    }

    videoSkipEvent(){
        this.seeked = this.videoElement.addEventListener('seeked',()=>{
            this.observer.disconnect();
            this.commentget_observer();
        });
        this.skip = document.querySelector('body:not(input)').addEventListener('keyup',(e)=>{
            if(e.code === 'ArrowRight' || e.code === 'ArrowLeft'){
                setTimeout(()=>{
                    this.observer.disconnect();
                    this.commentget_observer();

                },500)
            }
        });
    }

}

const comeScroll = new CommentScroll(setting, HTML);

let oldUrl = '';


setInterval(()=>{
		if (oldUrl !== location.href) {
			oldUrl = location.href; // oldUrlを更新
			if (oldUrl.match(/https:\/\/kick\.com\/*/) !== null) {
				window.dispatchEvent(new CustomEvent('urlChange'));  // イベントを発火
			}
		}
},3000)


window.addEventListener('urlChange', () => {
	comeScroll.firstEvent();
});