您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
コメントをニコニコ風に流すやつ
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.gf.qytechs.cn/scripts/444119/1045035/FlowComments.js
- // ==UserScript==
- // @name FlowComments
- // @namespace midra.me
- // @version 0.0.2
- // @description コメントをニコニコ風に流すやつ
- // @author Midra
- // @license MIT
- // @grant none
- // @compatible chrome >=84
- // ==/UserScript==
- // @ts-check
- /* jshint esversion: 6 */
- 'use strict'
- /**
- * `FlowCommentItem`のオプション
- * @typedef {object} FlowCommentItemOption
- * @property {string} color フォントカラー
- * @property {number} fontScale 拡大率
- * @property {string} position 表示位置
- * @property {number} speed 速度
- * @property {number} opacity 透明度
- */
- /**
- * `FlowComments`のオプション
- * @typedef {object} FlowCommentsOption
- * @property {number} resolution 解像度
- * @property {number} limit 画面内に表示するコメントの最大数
- */
- /****************************************
- * @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}
- */
- startTime = null
- /**
- * 表示時間(期間)
- * @type {number}
- */
- #lifetime = 6000
- /**
- * オプション
- * @type {FlowCommentItemOption}
- */
- #option = {
- 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 }
- }
- get id() { return this.#id }
- get text() { return this.#text }
- get lifetime() { return this.#lifetime / this.#option.speed }
- 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
- /**
- * `AnimationFrame`の`requestID`
- * @type {number}
- */
- #animReqId = null
- /**
- * Canvas
- * @type {HTMLCanvasElement}
- */
- #canvas
- /**
- * CanvasRenderingContext2D
- * @type {CanvasRenderingContext2D}
- */
- #context2d
- /**
- * 現在表示中のコメント
- * @type {Array<FlowCommentItem>}
- */
- #comments
- /**
- * オプション
- * @type {FlowCommentsOption}
- */
- #option = {
- resolution: 720,
- limit: 0,
- }
- /****************************************
- * コンストラクタ
- * @param {?FlowCommentsOption} option オプション
- */
- constructor(option = null) {
- this.#id = ++FlowComments.#id_cnt
- this.#canvas = document.createElement('canvas')
- this.#canvas.classList.add('mid-FlowComment')
- this.#canvas.dataset.fcid = this.#id.toString()
- 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 / 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'
- }
- get isStarted() { return this.#animReqId !== null }
- /****************************************
- * 初期化(インスタンス生成時には不要)
- * @param {?FlowCommentsOption} option オプション
- */
- initialize(option = null) {
- 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.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.#option.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.#option.resolution
- this.#canvas.height = this.#option.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 === 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(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 {FlowCommentItem} comment コメント
- */
- #renderComment(comment) {
- this.#context2d.fillText(comment.text, comment.x, comment.y)
- }
- /****************************************
- * ループ中に実行される処理
- * @param {number} nowTime 時間
- */
- #update(nowTime) {
- // Canvasをリセット
- this.#context2d.clearRect(0, 0, this.#canvas.width, this.#canvas.height)
- const deleteIdx = []
- this.#comments.forEach((comment, idx) => {
- // コメントを流し始めた時間
- if (comment.startTime === null) {
- comment.startTime = nowTime
- }
- // コメントを流し始めて経過した時間
- const elapsedTime = nowTime - comment.startTime
- if (elapsedTime <= comment.lifetime * 1.5) {
- // コメントの座標を更新
- comment.xp = elapsedTime / comment.lifetime
- comment.x = this.#canvas.width - comment.scrollWidth * comment.xp
- // コメントを描画
- this.#renderComment(comment)
- } else {
- // 表示時間を超えたら消す
- deleteIdx.push(idx)
- }
- })
- // 上のループが終わってから消さないと変な挙動になる
- deleteIdx.forEach(v => this.#comments.splice(v, 1))
- }
- /****************************************
- * ループ処理
- */
- #loop() {
- this.#update(window.performance.now())
- 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
- }
- }
- }
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址