FlowComments

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

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

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

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