Greasy Fork 还支持 简体中文。

FlowComments

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

目前為 2022-04-28 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/444119/1044798/FlowComments.js

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