FlowComments

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

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

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

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