FlowComments

コメントをニコニコ風に流すやつ

当前为 2022-04-28 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.gf.qytechs.cn/scripts/444119/1045035/FlowComments.js

  1. // ==UserScript==
  2. // @name FlowComments
  3. // @namespace midra.me
  4. // @version 0.0.2
  5. // @description コメントをニコニコ風に流すやつ
  6. // @author Midra
  7. // @license MIT
  8. // @grant none
  9. // @compatible chrome >=84
  10. // ==/UserScript==
  11.  
  12. // @ts-check
  13. /* jshint esversion: 6 */
  14.  
  15. 'use strict'
  16.  
  17. /**
  18. * `FlowCommentItem`のオプション
  19. * @typedef {object} FlowCommentItemOption
  20. * @property {string} color フォントカラー
  21. * @property {number} fontScale 拡大率
  22. * @property {string} position 表示位置
  23. * @property {number} speed 速度
  24. * @property {number} opacity 透明度
  25. */
  26.  
  27. /**
  28. * `FlowComments`のオプション
  29. * @typedef {object} FlowCommentsOption
  30. * @property {number} resolution 解像度
  31. * @property {number} limit 画面内に表示するコメントの最大数
  32. */
  33.  
  34. /****************************************
  35. * @classdesc 流すコメント
  36. * @example
  37. * // idを指定する場合
  38. * const fcItem1 = new FlowCommentItem('1518633760656605184', 'ウルトラソウッ')
  39. * // idを指定しない場合
  40. * const fcItem2 = new FlowCommentItem(Symbol(), 'うんち!')
  41. */
  42. class FlowCommentItem {
  43. /**
  44. * コメントID
  45. * @type {string | number | symbol}
  46. */
  47. #id
  48. /**
  49. * コメント本文
  50. * @type {string}
  51. */
  52. #text
  53. /**
  54. * X座標
  55. * @type {number}
  56. */
  57. x = 0
  58. /**
  59. * X座標(割合)
  60. * @type {number}
  61. */
  62. xp = 0
  63. /**
  64. * Y座標
  65. * @type {number}
  66. */
  67. y = 0
  68. /**
  69. * コメントの幅
  70. * @type {number}
  71. */
  72. width = 0
  73. /**
  74. * コメントの高さ
  75. * @type {number}
  76. */
  77. height = 0
  78. /**
  79. * 実際に流すときの距離
  80. * @type {number}
  81. */
  82. scrollWidth = 0
  83. /**
  84. * 行番号
  85. * @type {number}
  86. */
  87. line = 0
  88. /**
  89. * コメントを流し始めた時間
  90. * @type {number}
  91. */
  92. startTime = null
  93. /**
  94. * 表示時間(期間)
  95. * @type {number}
  96. */
  97. #lifetime = 6000
  98. /**
  99. * オプション
  100. * @type {FlowCommentItemOption}
  101. */
  102. #option = {
  103. color: '#fff',
  104. fontScale: 1,
  105. position: 'flow',
  106. speed: 1,
  107. opacity: 0,
  108. }
  109.  
  110. /****************************************
  111. * コンストラクタ
  112. * @param {string | number | symbol} id コメントID
  113. * @param {string} text コメント本文
  114. * @param {?FlowCommentItemOption} option オプション
  115. */
  116. constructor(id, text, option = null) {
  117. this.#id = id
  118. this.#text = text
  119. this.#option = { ...this.#option, ...option }
  120. }
  121.  
  122. get id() { return this.#id }
  123. get text() { return this.#text }
  124. get lifetime() { return this.#lifetime / this.#option.speed }
  125.  
  126. get top() { return this.y }
  127. get bottom() { return this.y + this.height }
  128. get left() { return this.x }
  129. get right() { return this.x + this.width }
  130. }
  131.  
  132. /****************************************
  133. * @classdesc コメントを流すやつ
  134. * @example
  135. * // 準備
  136. * const fc = new FlowComments()
  137. * document.body.appendChild(fc.canvas)
  138. * fc.start()
  139. *
  140. * // コメントを流す(追加する)
  141. * fc.pushComment(new FlowCommentItem(Symbol(), 'Hello, world!'))
  142. */
  143. class FlowComments {
  144. /**
  145. * インスタンスに割り当てられるIDのカウント用
  146. * @type {number}
  147. */
  148. static #id_cnt = 0
  149. /**
  150. * インスタンスに割り当てられるID
  151. * @type {number}
  152. */
  153. #id
  154. /**
  155. * `AnimationFrame`の`requestID`
  156. * @type {number}
  157. */
  158. #animReqId = null
  159. /**
  160. * Canvas
  161. * @type {HTMLCanvasElement}
  162. */
  163. #canvas
  164. /**
  165. * CanvasRenderingContext2D
  166. * @type {CanvasRenderingContext2D}
  167. */
  168. #context2d
  169. /**
  170. * 現在表示中のコメント
  171. * @type {Array<FlowCommentItem>}
  172. */
  173. #comments
  174. /**
  175. * オプション
  176. * @type {FlowCommentsOption}
  177. */
  178. #option = {
  179. resolution: 720,
  180. limit: 0,
  181. }
  182.  
  183. /****************************************
  184. * コンストラクタ
  185. * @param {?FlowCommentsOption} option オプション
  186. */
  187. constructor(option = null) {
  188. this.#id = ++FlowComments.#id_cnt
  189. this.#canvas = document.createElement('canvas')
  190. this.#canvas.classList.add('mid-FlowComment')
  191. this.#canvas.dataset.fcid = this.#id.toString()
  192. this.#context2d = this.#canvas.getContext('2d')
  193. this.initialize(option)
  194. }
  195.  
  196. get id() { return this.#id }
  197. get option() { return this.#option }
  198. get canvas() { return this.#canvas }
  199. get context2d() { return this.#context2d }
  200. get comments() { return this.#comments }
  201.  
  202. get lineHeight() { return this.#canvas.height / 11.4 }
  203. get lineSpace() { return this.lineHeight * 0.4 }
  204. get fontSize() { return this.lineHeight - this.lineSpace * 0.5 }
  205. get fontFamily() {
  206. return 'Arial,"MS Pゴシック","MS PGothic",MSPGothic,MS-PGothic,Gulim,"黑体",SimHei'
  207. }
  208.  
  209. get isStarted() { return this.#animReqId !== null }
  210.  
  211. /****************************************
  212. * 初期化(インスタンス生成時には不要)
  213. * @param {?FlowCommentsOption} option オプション
  214. */
  215. initialize(option = null) {
  216. this.stop()
  217. this.#option = { ...this.#option, ...option }
  218. this.#comments = []
  219. this.#animReqId = null
  220. this.initializeCanvas()
  221. }
  222.  
  223. /****************************************
  224. * Canvasの解像度を変更
  225. * @param {number} resolution 解像度
  226. */
  227. changeResolution(resolution) {
  228. if (Number.isFinite(resolution)) {
  229. this.#option.resolution = resolution
  230. this.initializeCanvas()
  231. }
  232. }
  233.  
  234. /****************************************
  235. * CanvasRenderingContext2Dを初期化
  236. */
  237. initializeCanvas() {
  238. this.#resizeCanvas()
  239. this.#context2d.clearRect(0, 0, this.#canvas.width, this.#canvas.height)
  240. this.#context2d.font = `600 ${this.fontSize}px ${this.fontFamily}`
  241. this.#context2d.lineJoin = 'round'
  242. this.#context2d.fillStyle = '#fff'
  243. this.#context2d.shadowColor = '#000'
  244. this.#context2d.shadowBlur = this.#option.resolution / 200
  245. this.#comments.forEach(this.#calcCommentProperty.bind(this))
  246. }
  247.  
  248. /****************************************
  249. * CanvasRenderingContext2Dをリサイズ
  250. */
  251. #resizeCanvas() {
  252. const { width, height } = this.#canvas.getBoundingClientRect()
  253. const ratio = (width === 0 && height === 0) ? (16 / 9) : (width / height)
  254. this.#canvas.width = ratio * this.#option.resolution
  255. this.#canvas.height = this.#option.resolution
  256. }
  257.  
  258. /****************************************
  259. * コメントの各プロパティを計算する
  260. * @param {FlowCommentItem} comment コメント
  261. */
  262. #calcCommentProperty(comment) {
  263. comment.width = this.#context2d.measureText(comment.text).width
  264. comment.scrollWidth = this.#canvas.width + comment.width
  265. comment.x = this.#canvas.width - comment.scrollWidth * comment.xp
  266. comment.y = this.lineHeight * comment.line
  267. }
  268.  
  269. /****************************************
  270. * コメントを追加(流す)
  271. * @param {FlowCommentItem} comment コメント
  272. */
  273. pushComment(comment) {
  274. if (this.#animReqId === null) return
  275.  
  276. //----------------------------------------
  277. // 画面内に表示するコメントを制限
  278. //----------------------------------------
  279. if (0 < this.#option.limit && this.#option.limit <= this.#comments.length) {
  280. this.#comments.splice(0, 1)
  281. }
  282.  
  283. //----------------------------------------
  284. // コメントの各プロパティを計算
  285. //----------------------------------------
  286. this.#calcCommentProperty(comment)
  287.  
  288. //----------------------------------------
  289. // コメント表示行を計算
  290. //----------------------------------------
  291. const spd_pushCmt = comment.scrollWidth / comment.lifetime
  292.  
  293. // [[1, 2], [2, 1], ~ , [11, 1]] ([line, cnt])
  294. const lines_over = [...Array(11)].map((_, i) => [i + 1, 0])
  295.  
  296. this.#comments.forEach(val => {
  297. // 残り表示時間
  298. const leftTime = val.lifetime * (1 - val.xp)
  299. // コメント追加時に重なる or 重なる予定かどうか
  300. const isOver =
  301. comment.left - spd_pushCmt * leftTime <= 0 ||
  302. comment.left <= val.right
  303. if (isOver) {
  304. lines_over[val.line - 1][1]++
  305. }
  306. })
  307.  
  308. // 重なった頻度を元に昇順で並べ替える
  309. const lines_sort = lines_over.sort(([, cntA], [, cntB]) => cntA - cntB)
  310.  
  311. comment.line = lines_sort[0][0]
  312. comment.y = this.lineHeight * comment.line
  313.  
  314. //----------------------------------------
  315. // コメントを追加
  316. //----------------------------------------
  317. this.#comments.push(comment)
  318. }
  319.  
  320. /****************************************
  321. * テキストを描画
  322. * @param {FlowCommentItem} comment コメント
  323. */
  324. #renderComment(comment) {
  325. this.#context2d.fillText(comment.text, comment.x, comment.y)
  326. }
  327.  
  328. /****************************************
  329. * ループ中に実行される処理
  330. * @param {number} nowTime 時間
  331. */
  332. #update(nowTime) {
  333. // Canvasをリセット
  334. this.#context2d.clearRect(0, 0, this.#canvas.width, this.#canvas.height)
  335.  
  336. const deleteIdx = []
  337. this.#comments.forEach((comment, idx) => {
  338. // コメントを流し始めた時間
  339. if (comment.startTime === null) {
  340. comment.startTime = nowTime
  341. }
  342.  
  343. // コメントを流し始めて経過した時間
  344. const elapsedTime = nowTime - comment.startTime
  345.  
  346. if (elapsedTime <= comment.lifetime * 1.5) {
  347. // コメントの座標を更新
  348. comment.xp = elapsedTime / comment.lifetime
  349. comment.x = this.#canvas.width - comment.scrollWidth * comment.xp
  350. // コメントを描画
  351. this.#renderComment(comment)
  352. } else {
  353. // 表示時間を超えたら消す
  354. deleteIdx.push(idx)
  355. }
  356. })
  357. // 上のループが終わってから消さないと変な挙動になる
  358. deleteIdx.forEach(v => this.#comments.splice(v, 1))
  359. }
  360.  
  361. /****************************************
  362. * ループ処理
  363. */
  364. #loop() {
  365. this.#update(window.performance.now())
  366. if (this.#animReqId !== null) {
  367. this.#animReqId = window.requestAnimationFrame(this.#loop.bind(this))
  368. }
  369. }
  370.  
  371. /****************************************
  372. * コメント流しを開始
  373. */
  374. start() {
  375. if (this.#animReqId === null) {
  376. this.#animReqId = window.requestAnimationFrame(this.#loop.bind(this))
  377. }
  378. }
  379.  
  380. /****************************************
  381. * コメント流しを停止
  382. */
  383. stop() {
  384. if (this.#animReqId !== null) {
  385. window.cancelAnimationFrame(this.#animReqId)
  386. this.#animReqId = null
  387. }
  388. }
  389. }

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址