您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
コメントをニコニコ風に流すやつ
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greatest.deepsurf.us/scripts/444119/1046016/FlowComments.js
// ==UserScript== // @name FlowComments // @namespace https://midra.me // @version 0.0.8 // @description コメントをニコニコ風に流すやつ // @author Midra // @license MIT // @grant none // @compatible chrome >=84 // @compatible safari >=15 // ==/UserScript== // @ts-check /* jshint esversion: 6 */ 'use strict' /** * `FlowCommentItem`のオプション * @typedef {object} FlowCommentItemOption * @property {string} [fontFamily] フォント * @property {string} [color] フォントカラー * @property {number} [fontScale] 拡大率 * @property {string} [position] 表示位置 * @property {number} [speed] 速度 * @property {number} [opacity] 透明度 */ /** * `FlowComments`のオプション * @typedef {object} FlowCommentsOption * @property {number} [resolution] 解像度 * @property {number} [lines] 行数 * @property {number} [limit] 画面内に表示するコメントの最大数 * @property {boolean} [autoResize] サイズ(比率)を自動で調整 * @property {boolean} [autoAdjRes] 解像度を自動で調整 */ /**************************************** * @classdesc 流すコメント * @example * // idを指定する場合 * const fcItem1 = new FlowCommentItem('1518633760656605184', 'ウルトラソウッ') * // idを指定しない場合 * const fcItem2 = new FlowCommentItem(Symbol(), 'うんち!') */ class FlowCommentItem { /** * 表示時間 * @type {number} */ static #lifetime = 6000 /** * コメントID * @type {string | number | symbol} */ #id /** * コメント本文 * @type {string} */ #text /** * X座標 * @type {number} */ x = 0 /** * X座標(割合) * @type {number} */ xp = 0 /** * Y座標 * @type {number} */ y = 0 /** * コメントの幅 * @type {number} */ width = 0 /** * コメントの高さ * @type {number} */ height = 0 /** * 実際に流すときの距離 * @type {number} */ scrollWidth = 0 /** * 行番号 * @type {number} */ line = 0 /** * コメントを流し始めた時間 * @type {number} */ startTime = null /** * 実際の表示時間 * @type {number} */ #actualLifetime /** * オプション * @type {FlowCommentItemOption} */ #option = { fontFamily: FlowComments.fontFamily, color: '#fff', fontScale: 1, position: 'flow', speed: 1, opacity: 0, } /**************************************** * コンストラクタ * @param {string | number | symbol} id コメントID * @param {string} text コメント本文 * @param {FlowCommentItemOption} [option] オプション */ constructor(id, text, option = null) { this.#id = id this.#text = text this.#option = { ...this.#option, ...option } this.#actualLifetime = FlowCommentItem.#lifetime * (this.#option.position === 'flow' ? 1.5 : 1) } get id() { return this.#id } get text() { return this.#text } get lifetime() { return FlowCommentItem.#lifetime / this.#option.speed } get actualLifetime() { return this.#actualLifetime } get option() { return this.#option } get top() { return this.y } get bottom() { return this.y + this.height } get left() { return this.x } get right() { return this.x + this.width } } /**************************************** * @classdesc コメントを流すやつ * @example * // 準備 * const fc = new FlowComments() * document.body.appendChild(fc.canvas) * fc.start() * * // コメントを流す(追加する) * fc.pushComment(new FlowCommentItem(Symbol(), 'Hello, world!')) */ class FlowComments { /** * インスタンスに割り当てられるIDのカウント用 * @type {number} */ static #id_cnt = 0 /** * デフォルトのフォント * @type {string} */ static fontFamily = 'Arial,"MS Pゴシック","MS PGothic",MSPGothic,MS-PGothic,sans-serif,-apple-system,Gulim,"黑体",SimHei' /** * インスタンスに割り当てられるID * @type {number} */ #id /** * `requestAnimationFrame`の`requestID` * @type {number} */ #animReqId = null /** * Canvas * @type {HTMLCanvasElement} */ #canvas /** * CanvasRenderingContext2D * @type {CanvasRenderingContext2D} */ #context2d /** * 現在表示中のコメント * @type {Array<FlowCommentItem>} */ #comments /** * オプション * @type {FlowCommentsOption} */ #option = { resolution: 720, lines: 11, limit: 0, autoResize: true, autoAdjRes: true, } /** * @type {ResizeObserver} */ #resizeObs /**************************************** * コンストラクタ * @param {FlowCommentsOption} [option] オプション */ constructor(option) { // ID割り当て this.#id = ++FlowComments.#id_cnt // Canvas生成 this.#canvas = document.createElement('canvas') this.#canvas.classList.add('mid-FlowComments') this.#canvas.dataset.fcid = this.#id.toString() // サイズ変更を監視 this.#resizeObs = new ResizeObserver(entries => { const { width, height } = entries[0].contentRect // Canvasのサイズ(比率)を自動で調整 if (this.#option.autoResize) { const rect_before = this.#canvas.width / this.#canvas.height const rect_resized = width / height if (0.01 < Math.abs(rect_before - rect_resized)) { this.#resizeCanvas() } } // Canvasの解像度を自動で調整 if (this.#option.autoAdjRes) { if (height <= 240) { this.changeResolution(240) } else if (height <= 480) { this.changeResolution(480) } else { this.changeResolution(720) } } }) this.#resizeObs.observe(this.#canvas) // CanvasRenderingContext2D this.#context2d = this.#canvas.getContext('2d') // 初期化 this.initialize(option) } get id() { return this.#id } get option() { return this.#option } get canvas() { return this.#canvas } get context2d() { return this.#context2d } get comments() { return this.#comments } get lineHeight() { return this.#canvas.height / (this.#option.lines + 0.5) } get lineSpace() { return this.lineHeight * 0.5 } get fontSize() { return this.lineHeight - this.lineSpace * 0.5 } get isStarted() { return this.#animReqId !== null } /**************************************** * 初期化(インスタンス生成時には不要) * @param {FlowCommentsOption} [option] オプション */ initialize(option) { this.stop() this.#option = { ...this.#option, ...option } this.#comments = [] this.#animReqId = null this.initializeCanvas() } /**************************************** * Canvasの解像度を変更 * @param {number} resolution 解像度 */ changeResolution(resolution) { if (Number.isFinite(resolution) && this.#option.resolution !== resolution) { this.#option.resolution = resolution this.initializeCanvas() } } /**************************************** * CanvasRenderingContext2Dを初期化 */ initializeCanvas() { this.#resizeCanvas() this.#context2d.clearRect(0, 0, this.#canvas.width, this.#canvas.height) } /**************************************** * CanvasRenderingContext2Dをリサイズ */ #resizeCanvas() { // Canvasをリサイズ const { width, height } = this.#canvas.getBoundingClientRect() const ratio = (width === 0 && height === 0) ? (16 / 9) : (width / height) this.#canvas.width = ratio * this.#option.resolution this.#canvas.height = this.#option.resolution // Canvasのスタイルをリセット this.#resetCanvasStyle() // コメントの各プロパティを再計算 this.#comments.forEach(this.#calcCommentProperty.bind(this)) } /**************************************** * Canvasのスタイルをリセット */ #resetCanvasStyle() { this.#context2d.font = `600 ${this.fontSize}px ${FlowComments.fontFamily}` this.#context2d.lineJoin = 'round' this.#context2d.fillStyle = '#fff' this.#context2d.shadowColor = '#000' this.#context2d.shadowBlur = this.#option.resolution / 200 } /**************************************** * コメントの各プロパティを計算する * @param {FlowCommentItem} comment コメント */ #calcCommentProperty(comment) { comment.width = this.#context2d.measureText(comment.text).width comment.scrollWidth = this.#canvas.width + comment.width comment.x = this.#canvas.width - comment.scrollWidth * comment.xp comment.y = this.lineHeight * comment.line } /**************************************** * コメントを追加(流す) * @param {FlowCommentItem} comment コメント */ pushComment(comment) { if (this.#animReqId === null) return //---------------------------------------- // 画面内に表示するコメントを制限 //---------------------------------------- if (0 < this.#option.limit && this.#option.limit <= this.#comments.length) { this.#comments.splice(0, 1) } //---------------------------------------- // コメントの各プロパティを計算 //---------------------------------------- this.#calcCommentProperty(comment) //---------------------------------------- // コメント表示行を計算 //---------------------------------------- const spd_pushCmt = comment.scrollWidth / comment.lifetime // [[1, 2], [2, 1], ~ , [11, 1]] ([line, cnt]) const lines_over = [...Array(this.#option.lines)].map((_, i) => [i + 1, 0]) this.#comments.forEach(val => { // 残り表示時間 const leftTime = val.lifetime * (1 - val.xp) // コメント追加時に重なる or 重なる予定かどうか const isOver = comment.left - spd_pushCmt * leftTime <= 0 || comment.left <= val.right if (isOver) { lines_over[val.line - 1][1]++ } }) // 重なった頻度を元に昇順で並べ替える const lines_sort = lines_over.sort(([, cntA], [, cntB]) => cntA - cntB) comment.line = lines_sort[0][0] comment.y = this.lineHeight * comment.line //---------------------------------------- // コメントを追加 //---------------------------------------- this.#comments.push(comment) } /**************************************** * テキストを描画 * @param {FlowCommentItem} comment コメント */ #renderComment(comment) { this.#context2d.fillText(comment.text, ~~comment.x, ~~comment.y) } /**************************************** * ループ中に実行される処理 * @param {number} time 時間 */ #update(time) { // Canvasをリセット this.#context2d.clearRect(0, 0, this.#canvas.width, this.#canvas.height) this.#comments.forEach((comment, idx, ary) => { // コメントを流し始めた時間 if (comment.startTime === null) { comment.startTime = time } // コメントを流し始めて経過した時間 const elapsedTime = time - comment.startTime if (elapsedTime <= comment.actualLifetime) { // コメントの座標を更新(流すコメント) if (comment.option.position === 'flow') { comment.xp = elapsedTime / comment.lifetime comment.x = this.#canvas.width - comment.scrollWidth * comment.xp } // コメントを描画 this.#renderComment(comment) } else { // 表示時間を超えたら消す ary.splice(idx, 1) } }) } /**************************************** * ループ処理 * @param {number} time 時間 */ #loop(time) { this.#update(time) if (this.#animReqId !== null) { this.#animReqId = window.requestAnimationFrame(this.#loop.bind(this)) } } /**************************************** * コメント流しを開始 */ start() { if (this.#animReqId === null) { this.#animReqId = window.requestAnimationFrame(this.#loop.bind(this)) } } /**************************************** * コメント流しを停止 */ stop() { if (this.#animReqId !== null) { window.cancelAnimationFrame(this.#animReqId) this.#animReqId = null } } /**************************************** * 解放(初期化してCanvasを削除) */ dispose() { this.initialize() this.#resizeObs.unobserve(this.#canvas) this.#resizeObs = null this.#canvas.remove() this.#canvas = null } }