此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.greasyfork.cloud/scripts/444119/1044798/FlowComments.js
您需要先安装一款用户样式管理器扩展(如 Stylus)后才能安装此样式。
您需要先安装一款用户样式管理器扩展(如 Stylus)后才能安装此样式。
您需要先安装一款用户样式管理器扩展(如 Stylus)后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
(我已经安装了用户样式管理器,让我安装!)
// ==UserScript==
// @name FlowComments
// @namespace midra.me
// @version 0.0.1
// @description コメントをニコニコ風に流すやつ
// @author Midra
// @license MIT
// @compatible chrome >=84
// @grant none
// ==/UserScript==
/* jshint esversion: 6 */
'use strict'
/****************************************
* 型定義
* @typedef {{
* resolution: number,
* opacity: number,
* limit: number,
* }} FlowCommentItemOption
*
* @typedef {{
* resolution: number,
* opacity: number,
* limit: number,
* }} FlowCommentOption
*/
/****************************************
* @classdesc 流すコメント
* @example
* // idを指定する場合
* const fcItem1 = new FlowCommentItem('1518633760656605184', 'ウルトラソウッ')
* // idを指定しない場合
* const fcItem2 = new FlowCommentItem(Symbol(), 'うんち!')
*/
class FlowCommentItem {
/**
* コメント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}
*/
lifetime = 6000
/**
* コメントを流し始めた時間
* @type {number}
*/
startTime
/****************************************
* コンストラクタ
* @param {string | number | symbol} id コメントID
* @param {string} text コメント本文
* @param {?FlowCommentItemOption} option オプション
*/
constructor(id, text, option) {
this.#id = id
this.#text = text
}
get id() { return this.#id }
get text() { return this.#text }
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
/**
* インスタンスに割り当てられるID
* @type {number}
*/
#id
/**
* オプション
* @type {FlowCommentOption}
*/
#options = {
resolution: 720,
opacity: 1,
limit: undefined,
}
/**
* Canvas
* @type {HTMLCanvasElement}
*/
#canvas
/**
* CanvasRenderingContext2D
* @type {CanvasRenderingContext2D}
*/
#context2d
/**
* 現在表示中のコメント
* @type {Array<FlowCommentItem>}
*/
#comments
/**
* `AnimationFrame`の`requestID`
* @type {number}
*/
#_animReqId
/****************************************
* コンストラクタ
* @param {FlowCommentOption} options オプション
*/
constructor(options) {
this.#id = ++FlowComments.#id_cnt
this.#canvas = document.createElement('canvas')
this.#canvas.classList.add('mid-FlowComment')
this.#canvas.dataset.fcid = this.#id
this.#context2d = this.#canvas.getContext('2d')
this.initialize(options)
}
get id() { return this.#id }
get options() { return this.#options }
get canvas() { return this.#canvas }
get context2d() { return this.#context2d }
get comments() { return this.#comments }
get lineHeight() { return this.#canvas.height / 11.4 }
get lineSpace() { return this.lineHeight * 0.4 }
get fontSize() { return this.lineHeight - this.lineSpace * 0.5 }
get fontFamily() {
return 'Arial,"MS Pゴシック","MS PGothic",MSPGothic,MS-PGothic,Gulim,"黑体",SimHei'
}
/****************************************
* 初期化(インスタンス生成時には不要)
* @param {FlowCommentOption} options オプション
*/
initialize(options) {
// オプションを設定
if (options !== undefined) {
if (options.resolution !== undefined) {
this.#options.resolution = options.resolution
}
if (options.opacity !== undefined) {
this.#options.opacity = options.opacity
}
if (options.limit !== undefined) {
this.#options.limit = options.limit
}
}
this.stop()
this.#comments = []
this.#_animReqId = undefined
this.initializeCanvas()
}
/****************************************
* Canvasの解像度を変更
* @param {number} resolution 解像度
*/
changeCanvasResolution(resolution) {
if (Number.isFinite(resolution)) {
this.#options.resolution = resolution
this.initializeCanvas()
}
}
/****************************************
* CanvasRenderingContext2Dを初期化
*/
initializeCanvas() {
this.#resizeCanvas()
this.#context2d.clearRect(0, 0, this.#canvas.width, this.#canvas.height)
this.#context2d.font = `600 ${this.fontSize}px ${this.fontFamily}`
this.#context2d.lineJoin = 'round'
this.#context2d.fillStyle = '#fff'
this.#context2d.shadowColor = '#000'
this.#context2d.shadowBlur = this.#options.resolution / 200
this.#comments.forEach(this.#calcCommentProperty.bind(this))
}
/****************************************
* CanvasRenderingContext2Dをリサイズ
*/
#resizeCanvas() {
const { width, height } = this.#canvas.getBoundingClientRect()
const ratio = (width === 0 && height === 0) ? (16 / 9) : (width / height)
this.#canvas.width = ratio * this.#options.resolution
this.#canvas.height = this.#options.resolution
}
/****************************************
* コメントの各プロパティを計算する
* @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 === undefined) return
//----------------------------------------
// 画面内に表示するコメントを制限
//----------------------------------------
if (this.#options.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(11)].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 {number} now 時間
*/
#render(now) {
// Canvasをリセット
this.#context2d.clearRect(0, 0, this.#canvas.width, this.#canvas.height)
const deleteIdx = []
this.#comments.forEach((comment, idx) => {
// コメントを流し始めた時間
if (comment.startTime === undefined) {
comment.startTime = now
}
// コメントを流し始めて経過した時間
const diffTime = now - comment.startTime
if (diffTime <= comment.lifetime * 1.5) {
// コメントの座標を更新
comment.xp = diffTime / comment.lifetime
comment.x = this.#canvas.width - comment.scrollWidth * comment.xp
// コメントを描画
this.#context2d.fillText(comment.text, comment.x, comment.y)
} else {
// 表示時間を超えたら消す
deleteIdx.push(idx)
}
})
// 上のループが終わってから消さないと変な挙動になる
deleteIdx.forEach(v => this.#comments.splice(v, 1))
}
/****************************************
* ループ処理
* @param {number} time 時間
*/
#loop() {
this.#render(window.performance.now())
if (this.#_animReqId !== undefined) {
this.#_animReqId = window.requestAnimationFrame(this.#loop.bind(this))
}
}
/****************************************
* コメント流しを開始
*/
start() {
if (this.#_animReqId === undefined) {
this.#_animReqId = window.requestAnimationFrame(this.#loop.bind(this))
}
}
/****************************************
* コメント流しを停止
*/
stop() {
if (this.#_animReqId !== undefined) {
window.cancelAnimationFrame(this.#_animReqId)
this.#_animReqId = undefined
}
}
}