FlowComments

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

当前为 2022-05-18 提交的版本,查看 最新版本

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

  1. // ==UserScript==
  2. // @name FlowComments
  3. // @namespace https://midra.me
  4. // @version 1.0.5
  5. // @description コメントをニコニコ風に流すやつ
  6. // @author Midra
  7. // @license MIT
  8. // @grant none
  9. // @compatible chrome >= 84
  10. // @compatible safari >= 15
  11. // @compatible firefox >= 90
  12. // ==/UserScript==
  13.  
  14. // @ts-check
  15.  
  16. 'use strict'
  17.  
  18. /**
  19. * `FlowComments`のスタイル
  20. * @typedef {object} FlowCommentsStyle
  21. * @property {string} [fontFamily] フォント
  22. * @property {string} [fontWeight] フォントの太さ
  23. * @property {number} [fontScale] 拡大率
  24. * @property {string} [color] フォントカラー
  25. * @property {string} [shadowColor] シャドウの色
  26. * @property {number} [shadowBlur] シャドウのぼかし
  27. * @property {number} [opacity] 透明度
  28. */
  29.  
  30. /**
  31. * `FlowCommentsItem`のオプション
  32. * @typedef {object} FlowCommentsItemOption
  33. * @property {number} [position] 表示位置
  34. * @property {number} [duration] 表示時間
  35. */
  36.  
  37. /**
  38. * `FlowComments`のオプション
  39. * @typedef {object} FlowCommentsOption
  40. * @property {number} [resolution] 解像度
  41. * @property {number} [lines] 行数
  42. * @property {number} [limit] 画面内に表示するコメントの最大数
  43. * @property {boolean} [autoResize] サイズ(比率)を自動で調整
  44. * @property {boolean} [autoResolution] 解像度を自動で調整
  45. */
  46.  
  47. /****************************************
  48. * デフォルト値
  49. */
  50. const FLOWCMT_CONFIG = Object.freeze({
  51. /** フォントファミリー */
  52. FONT_FAMILY: [
  53. 'Arial',
  54. '"ヒラギノ角ゴシック"', '"Hiragino Sans"',
  55. '"游ゴシック体"', 'YuGothic', '"游ゴシック"', '"Yu Gothic"',
  56. 'Gulim', '"Malgun Gothic"',
  57. '"黑体"', 'SimHei',
  58. 'system-ui', '-apple-system',
  59. 'sans-serif',
  60. ].join(),
  61.  
  62. /** フォントの太さ */
  63. FONT_WEIGHT: /Android/.test(window.navigator.userAgent) ? '700' : '600',
  64.  
  65. /** フォントの拡大率 */
  66. FONT_SCALE: 0.7,
  67.  
  68. /** フォントのY軸のオフセット */
  69. FONT_OFFSET_Y: 0.15,
  70.  
  71. /** テキストの色 */
  72. TEXT_COLOR: '#fff',
  73.  
  74. /** テキストシャドウの色 */
  75. TEXT_SHADOW_COLOR: '#000',
  76.  
  77. /** テキストシャドウのぼかし */
  78. TEXT_SHADOW_BLUR: 1,
  79.  
  80. /** テキスト間の余白(配列形式の場合) */
  81. TEXT_MARGIN: 0.2,
  82.  
  83. /** Canvasのクラス名 */
  84. CANVAS_CLASSNAME: 'mid-FlowComments',
  85.  
  86. /** Canvasの比率 */
  87. CANVAS_RATIO: 16 / 9,
  88.  
  89. /** Canvasの解像度 */
  90. CANVAS_RESOLUTION: 720,
  91.  
  92. /** 解像度のリスト */
  93. RESOLUTION_LIST: [240, 360, 480, 720],
  94.  
  95. /** コメントの表示時間 */
  96. CMT_DISPLAY_DURATION: 6000,
  97.  
  98. /** コメントの最大数(0は無制限) */
  99. CMT_LIMIT: 0,
  100.  
  101. /** 行数 */
  102. LINES: 11,
  103.  
  104. /** 比率の自動調整 */
  105. AUTO_RESIZE: true,
  106.  
  107. /** 解像度の自動調整 */
  108. AUTO_RESOLUTION: true,
  109. })
  110.  
  111. /****************************************
  112. * コメントの種類
  113. */
  114. const FLOWCMT_TYPE = Object.freeze({
  115. FLOW: 0,
  116. TOP: 1,
  117. BOTTOM: 2,
  118. })
  119.  
  120. /****************************************
  121. * @type {FlowCommentsItemOption}
  122. */
  123. const FLOWCMTITEM_DEFAULT_OPTION = Object.freeze({
  124. position: FLOWCMT_TYPE.FLOW,
  125. duration: FLOWCMT_CONFIG.CMT_DISPLAY_DURATION,
  126. })
  127.  
  128. /****************************************
  129. * @type {FlowCommentsStyle}
  130. */
  131. const FLOWCMT_DEFAULT_STYLE = Object.freeze({
  132. fontFamily: FLOWCMT_CONFIG.FONT_FAMILY,
  133. fontWeight: FLOWCMT_CONFIG.FONT_WEIGHT,
  134. fontScale: 1,
  135. color: FLOWCMT_CONFIG.TEXT_COLOR,
  136. shadowColor: FLOWCMT_CONFIG.TEXT_SHADOW_COLOR,
  137. shadowBlur: FLOWCMT_CONFIG.TEXT_SHADOW_BLUR,
  138. opacity: 1,
  139. })
  140.  
  141. /****************************************
  142. * @type {FlowCommentsOption}
  143. */
  144. const FLOWCMT_DEFAULT_OPTION = Object.freeze({
  145. resolution: FLOWCMT_CONFIG.CANVAS_RESOLUTION,
  146. lines: FLOWCMT_CONFIG.LINES,
  147. limit: FLOWCMT_CONFIG.CMT_LIMIT,
  148. autoResize: FLOWCMT_CONFIG.AUTO_RESIZE,
  149. autoResolution: FLOWCMT_CONFIG.AUTO_RESOLUTION,
  150. })
  151.  
  152. /****************************************
  153. * @classdesc ユーティリティ
  154. */
  155. class FlowCommentsUtil {
  156. /****************************************
  157. * オブジェクトのプロパティからnullとundefinedを除去
  158. * @param {object} obj オブジェクト
  159. */
  160. static filterObject(obj) {
  161. if (typeof obj === 'object' && !Array.isArray(obj) && obj !== undefined && obj !== null) {
  162. Object.keys(obj).forEach(key => {
  163. if (obj[key] === undefined || obj[key] === null) {
  164. delete obj[key]
  165. } else {
  166. this.filterObject(obj[key])
  167. }
  168. })
  169. }
  170. }
  171.  
  172. /****************************************
  173. * Canvasにスタイルを適用
  174. * @param {CanvasRenderingContext2D} ctx CanvasRenderingContext2D
  175. * @param {FlowCommentsStyle} style スタイル
  176. * @param {number} resolution 解像度
  177. * @param {number} fontSize フォントサイズ
  178. */
  179. static setStyleToCanvas(ctx, style, resolution, fontSize) {
  180. ctx.textBaseline = 'middle'
  181. ctx.lineJoin = 'round'
  182. ctx.font = `${style.fontWeight} ${fontSize * style.fontScale}px ${style.fontFamily}`
  183. ctx.fillStyle = style.color
  184. ctx.shadowColor = style.shadowColor
  185. ctx.shadowBlur = resolution / 400 * style.shadowBlur
  186. ctx.globalAlpha = style.opacity
  187. }
  188. }
  189.  
  190. /****************************************
  191. * @classdesc 画像キャッシュ管理用
  192. */
  193. class FlowCommentsImageCache {
  194. /**
  195. * オプション(デフォルト値)
  196. */
  197. static #OPTION = {
  198. maxSize: 50,
  199. }
  200. /**
  201. * キャッシュ
  202. * @type {{ [url: string]: { img: HTMLImageElement; lastUsed: number; }; }}
  203. */
  204. static #cache = {}
  205.  
  206. /****************************************
  207. * キャッシュ追加
  208. * @param {string} url URL
  209. * @param {HTMLImageElement} img 画像
  210. */
  211. static add(url, img) {
  212. // 削除
  213. if (this.#OPTION.maxSize < Object.keys(this.#cache).length) {
  214. let delCacheUrl
  215. Object.keys(this.#cache).forEach(key => {
  216. if (
  217. delCacheUrl === undefined ||
  218. this.#cache[key].lastUsed < this.#cache[delCacheUrl].lastUsed
  219. ) {
  220. delCacheUrl = key
  221. }
  222. })
  223. this.dispose(delCacheUrl)
  224. }
  225.  
  226. // 追加
  227. this.#cache[url] = {
  228. img: img,
  229. lastUsed: Date.now(),
  230. }
  231. }
  232.  
  233. /****************************************
  234. * 画像が存在するか
  235. * @param {string} url URL
  236. */
  237. static has(url) {
  238. return this.#cache.hasOwnProperty(url)
  239. }
  240.  
  241. /****************************************
  242. * 画像を取得
  243. * @param {string} url URL
  244. * @returns {Promise<HTMLImageElement>} 画像
  245. */
  246. static async get(url) {
  247. return new Promise(async (resolve, reject) => {
  248. if (this.has(url)) {
  249. this.#cache[url].lastUsed = Date.now()
  250. resolve(this.#cache[url].img)
  251. } else {
  252. try {
  253. let img = new Image()
  254. img.addEventListener('load', ({ target }) => {
  255. if (target instanceof HTMLImageElement) {
  256. this.add(target.src, target)
  257. resolve(this.#cache[target.src].img)
  258. } else {
  259. reject()
  260. }
  261. })
  262. img.addEventListener('error', reject)
  263. img.src = url
  264. img = null
  265. } catch (e) {
  266. reject(e)
  267. }
  268. }
  269. })
  270. }
  271.  
  272. /****************************************
  273. * 画像を解放
  274. * @param {string} url URL
  275. */
  276. static dispose(url) {
  277. if (this.has(url)) {
  278. this.#cache[url].img.remove()
  279. delete this.#cache[url]
  280. }
  281. }
  282. }
  283.  
  284. /****************************************
  285. * @classdesc `FlowCommentsItem`用の画像クラス
  286. */
  287. class FlowCommentsImage {
  288. /**
  289. * URL
  290. * @type {string}
  291. */
  292. #url
  293. /**
  294. * 代替テキスト
  295. * @type {string}
  296. */
  297. #alt
  298.  
  299. /****************************************
  300. * コンストラクタ
  301. * @param {string} url URL
  302. * @param {string} [alt] 代替テキスト
  303. */
  304. constructor(url, alt) {
  305. this.#url = url
  306. this.#alt = alt
  307. }
  308.  
  309. get url() { return this.#url }
  310. get alt() { return this.#alt }
  311.  
  312. /****************************************
  313. * 画像を取得
  314. * @returns {Promise<HTMLImageElement | string>}
  315. */
  316. async get() {
  317. try {
  318. return (await FlowCommentsImageCache.get(this.#url))
  319. } catch (e) {
  320. return this.#alt
  321. }
  322. }
  323. }
  324.  
  325. /****************************************
  326. * @classdesc 流すコメント
  327. * @example
  328. * // idを指定する場合
  329. * const fcItem1 = new FlowCommentsItem('1518633760656605184', 'ウルトラソウッ')
  330. * // idを指定しない場合
  331. * const fcItem2 = new FlowCommentsItem(Symbol(), 'みどらんかわいい!')
  332. */
  333. class FlowCommentsItem {
  334. /**
  335. * コメントID
  336. * @type {string | number | symbol}
  337. */
  338. #id
  339. /**
  340. * コメント本文
  341. * @type {Array<string | FlowCommentsImage>}
  342. */
  343. #content
  344. /**
  345. * スタイル
  346. * @type {FlowCommentsStyle}
  347. */
  348. #style
  349. /**
  350. * オプション
  351. * @type {FlowCommentsItemOption}
  352. */
  353. #option
  354. /**
  355. * 実際の表示時間
  356. * @type {number}
  357. */
  358. #actualDuration
  359. /**
  360. * コメント単体を描画したCanvas
  361. * @type {HTMLCanvasElement}
  362. */
  363. #canvas
  364.  
  365. /**
  366. * 座標
  367. * @type {{ x: number; y: number; xp: number; offsetY: number; }}
  368. */
  369. position = {
  370. x: 0,
  371. y: 0,
  372. xp: 0,
  373. offsetY: 0,
  374. }
  375. /**
  376. * 描画サイズ
  377. * @type {{ width: number; height: number; }}
  378. */
  379. size = {
  380. width: 0,
  381. height: 0,
  382. }
  383. /**
  384. * 実際に流すときの距離
  385. * @type {number}
  386. */
  387. scrollWidth = 0
  388. /**
  389. * 行番号
  390. * @type {number}
  391. */
  392. line = 0
  393. /**
  394. * コメントを流し始めた時間
  395. * @type {number}
  396. */
  397. startTime = null
  398.  
  399. /****************************************
  400. * コンストラクタ
  401. * @param {string | number | symbol} id コメントID
  402. * @param {Array<string | FlowCommentsImage>} content コメント本文
  403. * @param {FlowCommentsItemOption} [option] オプション
  404. * @param {FlowCommentsStyle} [style] スタイル
  405. */
  406. constructor(id, content, option, style) {
  407. FlowCommentsUtil.filterObject(option)
  408. FlowCommentsUtil.filterObject(style)
  409. this.#id = id
  410. this.#content = Array.isArray(content) ? content.filter(v => v) : content
  411. this.#style = style
  412. this.#option = { ...FLOWCMTITEM_DEFAULT_OPTION, ...option }
  413. if (this.#option.position === FLOWCMT_TYPE.FLOW) {
  414. this.#actualDuration = this.#option.duration * 1.5
  415. }
  416. this.#canvas = document.createElement('canvas')
  417. }
  418.  
  419. get id() { return this.#id }
  420. get content() { return this.#content }
  421. get style() { return this.#style }
  422. get option() { return this.#option }
  423. get actualDuration() { return this.#actualDuration }
  424. get canvas() { return this.#canvas }
  425.  
  426. get top() { return this.position.y }
  427. get bottom() { return this.position.y + this.size.height }
  428. get left() { return this.position.x }
  429. get right() { return this.position.x + this.size.width }
  430.  
  431. get rect() {
  432. return {
  433. width: this.size.width,
  434. height: this.size.height,
  435. top: this.top,
  436. bottom: this.bottom,
  437. left: this.left,
  438. right: this.right,
  439. }
  440. }
  441.  
  442. dispose() {
  443. this.#canvas.remove()
  444.  
  445. this.#id = null
  446. this.#content = null
  447. this.#style = null
  448. this.#option = null
  449. this.#actualDuration = null
  450. this.#canvas = null
  451.  
  452. Object.keys(this).forEach(k => delete this[k])
  453. }
  454. }
  455.  
  456. /****************************************
  457. * @classdesc コメントを流すやつ
  458. * @example
  459. * // 準備
  460. * const fc = new FlowComments()
  461. * document.body.appendChild(fc.canvas)
  462. * fc.start()
  463. *
  464. * // コメントを流す(追加する)
  465. * fc.pushComment(new FlowCommentsItem(Symbol(), 'Hello world!'))
  466. */
  467. class FlowComments {
  468. /**
  469. * インスタンスに割り当てられるIDのカウント用
  470. * @type {number}
  471. */
  472. static #id_cnt = 0
  473.  
  474. /**
  475. * インスタンスに割り当てられるID
  476. * @type {number}
  477. */
  478. #id
  479. /**
  480. * `requestAnimationFrame`の`requestID`
  481. * @type {number}
  482. */
  483. #animReqId = null
  484. /**
  485. * Canvas
  486. * @type {HTMLCanvasElement}
  487. */
  488. #canvas
  489. /**
  490. * CanvasRenderingContext2D
  491. * @type {CanvasRenderingContext2D}
  492. */
  493. #context2d
  494. /**
  495. * 現在表示中のコメント
  496. * @type {Array<FlowCommentsItem>}
  497. */
  498. #comments
  499. /**
  500. * スタイル
  501. * @type {FlowCommentsStyle}
  502. */
  503. #style
  504. /**
  505. * オプション
  506. * @type {FlowCommentsOption}
  507. */
  508. #option
  509. /**
  510. * @type {ResizeObserver}
  511. */
  512. #resizeObs
  513.  
  514. /****************************************
  515. * コンストラクタ
  516. * @param {FlowCommentsOption} [option] オプション
  517. * @param {FlowCommentsStyle} [style] スタイル
  518. */
  519. constructor(option, style) {
  520. // 初期化
  521. this.initialize(option, style)
  522. }
  523.  
  524. get id() { return this.#id }
  525. get style() { return { ...FLOWCMT_DEFAULT_STYLE, ...this.#style } }
  526. get option() { return { ...FLOWCMT_DEFAULT_OPTION, ...this.#option } }
  527. get canvas() { return this.#canvas }
  528. get context2d() { return this.#context2d }
  529. get comments() { return this.#comments }
  530.  
  531. get lineHeight() { return this.#canvas.height / this.option.lines }
  532. get fontSize() { return this.lineHeight * FLOWCMT_CONFIG.FONT_SCALE }
  533.  
  534. get isStarted() { return this.#animReqId !== null }
  535.  
  536. /****************************************
  537. * 初期化(インスタンス生成時には不要)
  538. * @param {FlowCommentsOption} [option] オプション
  539. * @param {FlowCommentsStyle} [style] スタイル
  540. */
  541. initialize(option, style) {
  542. this.dispose()
  543.  
  544. // ID割り当て
  545. this.#id = ++FlowComments.#id_cnt
  546.  
  547. // Canvas生成
  548. this.#canvas = document.createElement('canvas')
  549. this.#canvas.classList.add(FLOWCMT_CONFIG.CANVAS_CLASSNAME)
  550. this.#canvas.dataset.fcid = this.#id.toString()
  551.  
  552. // CanvasRenderingContext2D
  553. this.#context2d = this.#canvas.getContext('2d')
  554.  
  555. // コメント一覧
  556. this.#comments = []
  557.  
  558. // サイズ変更を監視
  559. this.#resizeObs = new ResizeObserver(entries => {
  560. entries.forEach(entry => {
  561. const { width, height } = entry.contentRect
  562.  
  563. // Canvasのサイズ(比率)を自動で調整
  564. if (this.option.autoResize) {
  565. const rect_before = this.#canvas.width / this.#canvas.height
  566. const rect_resized = width / height
  567. if (0.01 < Math.abs(rect_before - rect_resized)) {
  568. this.#resizeCanvas()
  569. }
  570. }
  571.  
  572. // Canvasの解像度を自動で調整
  573. if (this.option.autoResolution) {
  574. const resolution = FLOWCMT_CONFIG.RESOLUTION_LIST.find(v => height <= v)
  575. if (Number.isFinite(resolution) && this.option.resolution !== resolution) {
  576. this.changeOption({ resolution: resolution })
  577. }
  578. }
  579. })
  580. })
  581. this.#resizeObs.observe(this.#canvas)
  582.  
  583. // オプションをセット
  584. this.changeOption(option)
  585. // スタイルをセット
  586. this.changeStyle(style)
  587. }
  588.  
  589. /****************************************
  590. * オプションを変更
  591. * @param {FlowCommentsOption} option オプション
  592. */
  593. changeOption(option) {
  594. FlowCommentsUtil.filterObject(option)
  595. this.#option = { ...this.#option, ...option }
  596. if (option !== undefined && option !== null) {
  597. this.#resizeCanvas()
  598. }
  599. }
  600.  
  601. /****************************************
  602. * スタイルを変更
  603. * @param {FlowCommentsStyle} [style] スタイル
  604. */
  605. changeStyle(style) {
  606. FlowCommentsUtil.filterObject(style)
  607. this.#style = { ...this.#style, ...style }
  608. if (style !== undefined && style !== null) {
  609. this.#updateCanvasStyle()
  610. }
  611. }
  612.  
  613. /****************************************
  614. * Canvasをリサイズ
  615. */
  616. #resizeCanvas() {
  617. // Canvasをリサイズ
  618. const { width, height } = this.#canvas.getBoundingClientRect()
  619. const { resolution } = this.option
  620. const ratio = (width === 0 && height === 0) ? FLOWCMT_CONFIG.CANVAS_RATIO : (width / height)
  621. this.#canvas.width = resolution * ratio
  622. this.#canvas.height = resolution
  623.  
  624. // Canvasのスタイルをリセット
  625. this.#updateCanvasStyle()
  626. }
  627.  
  628. /****************************************
  629. * Canvasのスタイルを更新
  630. */
  631. #updateCanvasStyle() {
  632. // スタイルを適用
  633. FlowCommentsUtil.setStyleToCanvas(
  634. this.#context2d, this.style, this.option.resolution, this.fontSize
  635. )
  636.  
  637. // Canvasをリセット
  638. this.#context2d.clearRect(0, 0, this.#canvas.width, this.#canvas.height)
  639. // コメントの各プロパティを再計算・描画
  640. this.#comments.forEach(cmt => {
  641. this.#generateCommentsItemCanvas(cmt)
  642. this.#renderComment(cmt)
  643. })
  644. }
  645.  
  646. /****************************************
  647. * Canvasのスタイルをリセット
  648. */
  649. resetCanvasStyle() {
  650. this.changeStyle(FLOWCMT_DEFAULT_STYLE)
  651. }
  652.  
  653. /****************************************
  654. * コメントの単体のCanvasを生成
  655. * @param {FlowCommentsItem} comment コメント
  656. */
  657. async #generateCommentsItemCanvas(comment) {
  658. const ctx = comment.canvas.getContext('2d')
  659. ctx.clearRect(0, 0, comment.canvas.width, comment.canvas.height)
  660.  
  661. const style = { ...this.style, ...comment.style }
  662. const drawFontSize = this.fontSize * style.fontScale
  663. const margin = drawFontSize * FLOWCMT_CONFIG.TEXT_MARGIN
  664.  
  665. // スタイルを適用
  666. FlowCommentsUtil.setStyleToCanvas(
  667. ctx, style, this.option.resolution, this.fontSize
  668. )
  669.  
  670. /** @type {Array<number>} */
  671. const aryWidth = []
  672.  
  673. //----------------------------------------
  674. // サイズを計算
  675. //----------------------------------------
  676. for (const cont of comment.content) {
  677. // 文字列
  678. if (typeof cont === 'string') {
  679. aryWidth.push(ctx.measureText(cont).width)
  680. }
  681. // 画像
  682. else if (cont instanceof FlowCommentsImage) {
  683. const img = await cont.get()
  684. if (img instanceof HTMLImageElement) {
  685. const ratio = img.width / img.height
  686. aryWidth.push(drawFontSize * ratio)
  687. } else if (img !== undefined) {
  688. aryWidth.push(ctx.measureText(img).width)
  689. } else {
  690. aryWidth.push(1)
  691. }
  692. }
  693. }
  694.  
  695. // コメントの各プロパティを計算
  696. comment.size.width = aryWidth.reduce((a, b) => a + b)
  697. comment.size.width += margin * (aryWidth.length - 1)
  698. comment.size.height = this.lineHeight
  699. comment.scrollWidth = this.#canvas.width + comment.size.width
  700. comment.position.x = this.#canvas.width - comment.scrollWidth * comment.position.xp
  701. comment.position.y = this.lineHeight * comment.line
  702. comment.position.offsetY = this.lineHeight / 2 * (1 + FLOWCMT_CONFIG.FONT_OFFSET_Y)
  703.  
  704. // Canvasのサイズを設定
  705. comment.canvas.width = comment.size.width
  706. comment.canvas.height = comment.size.height
  707.  
  708. // スタイルを再適用(上でリセットされる)
  709. FlowCommentsUtil.setStyleToCanvas(
  710. ctx, style, this.option.resolution, this.fontSize
  711. )
  712.  
  713. //----------------------------------------
  714. // コメントを描画
  715. //----------------------------------------
  716. let dx = 0
  717. for (let idx = 0; idx < comment.content.length; idx++) {
  718. if (0 < idx) {
  719. dx += margin
  720. }
  721. const cont = comment.content[idx]
  722. // 文字列
  723. if (typeof cont === 'string') {
  724. ctx.fillText(
  725. cont,
  726. (dx | 0), (comment.position.offsetY | 0)
  727. )
  728. }
  729. // 画像
  730. else if (cont instanceof FlowCommentsImage) {
  731. const img = await cont.get()
  732. if (img instanceof HTMLImageElement) {
  733. ctx.drawImage(
  734. img,
  735. (dx | 0), ((comment.size.height - drawFontSize) / 2 | 0),
  736. (aryWidth[idx] | 0), (drawFontSize | 0)
  737. )
  738. } else if (img !== undefined) {
  739. ctx.fillText(
  740. img,
  741. (dx | 0), (comment.position.offsetY | 0)
  742. )
  743. } else {
  744. ctx.fillText(
  745. '',
  746. (dx | 0), (comment.position.offsetY | 0)
  747. )
  748. }
  749. }
  750. dx += aryWidth[idx]
  751. }
  752. }
  753.  
  754. /****************************************
  755. * コメントを追加(流す)
  756. * @param {FlowCommentsItem} comment コメント
  757. */
  758. async pushComment(comment) {
  759. if (this.#animReqId === null || document.visibilityState === 'hidden') return
  760.  
  761. //----------------------------------------
  762. // 画面内に表示するコメントを制限
  763. //----------------------------------------
  764. if (0 < this.option.limit && this.option.limit <= this.#comments.length) {
  765. this.#comments.splice(0, this.#comments.length - this.option.limit)[0]
  766. }
  767.  
  768. //----------------------------------------
  769. // コメントの各プロパティを計算
  770. //----------------------------------------
  771. await this.#generateCommentsItemCanvas(comment)
  772.  
  773. //----------------------------------------
  774. // コメント表示行を計算
  775. //----------------------------------------
  776. const spd_pushCmt = comment.scrollWidth / comment.option.duration
  777.  
  778. // [[0, 0], [1, 0], ~ , [10, 0]] ([line, cnt])
  779. const lines_over = [...Array(this.option.lines)].map((_, i) => [i, 0])
  780.  
  781. this.#comments.forEach(cmt => {
  782. // 残り表示時間
  783. const leftTime = cmt.option.duration * (1 - cmt.position.xp)
  784. // コメント追加時に重なる or 重なる予定かどうか
  785. const isOver =
  786. comment.left - spd_pushCmt * leftTime <= 0 ||
  787. comment.left <= cmt.right
  788. if (isOver && cmt.line < this.option.lines) {
  789. lines_over[cmt.line][1]++
  790. }
  791. })
  792.  
  793. // 重なった頻度を元に昇順で並べ替える
  794. const lines_sort = lines_over.sort(([, cntA], [, cntB]) => cntA - cntB)
  795.  
  796. comment.line = lines_sort[0][0]
  797. comment.position.y = this.lineHeight * comment.line
  798.  
  799. //----------------------------------------
  800. // コメントを追加
  801. //----------------------------------------
  802. this.#comments.push(comment)
  803. }
  804.  
  805. /****************************************
  806. * テキストを描画
  807. * @param {FlowCommentsItem} comment コメント
  808. */
  809. #renderComment(comment) {
  810. this.#context2d.drawImage(
  811. comment.canvas,
  812. (comment.position.x | 0), (comment.position.y | 0)
  813. )
  814. }
  815.  
  816. /****************************************
  817. * ループ中に実行される処理
  818. * @param {number} time 時間
  819. */
  820. #update(time) {
  821. // Canvasをリセット
  822. this.#context2d.clearRect(0, 0, this.#canvas.width, this.#canvas.height)
  823.  
  824. this.#comments.forEach((cmt, idx, ary) => {
  825. // コメントを流し始めた時間
  826. if (cmt.startTime === null) {
  827. cmt.startTime = time
  828. }
  829.  
  830. // コメントを流し始めて経過した時間
  831. const elapsedTime = time - cmt.startTime
  832.  
  833. if (elapsedTime <= cmt.actualDuration) {
  834. // コメントの座標を更新(流すコメント)
  835. if (cmt.option.position === FLOWCMT_TYPE.FLOW) {
  836. cmt.position.xp = elapsedTime / cmt.option.duration
  837. cmt.position.x = this.#canvas.width - cmt.scrollWidth * cmt.position.xp
  838. }
  839. // コメントを描画
  840. this.#renderComment(cmt)
  841. } else {
  842. // 表示時間を超えたら消す
  843. cmt.dispose()
  844. ary.splice(idx, 1)[0]
  845. }
  846. })
  847. }
  848.  
  849. /****************************************
  850. * ループ処理
  851. * @param {number} time 時間
  852. */
  853. #loop(time) {
  854. this.#update(time)
  855. if (this.#animReqId !== null) {
  856. this.#animReqId = window.requestAnimationFrame(this.#loop.bind(this))
  857. }
  858. }
  859.  
  860. /****************************************
  861. * コメント流しを開始
  862. */
  863. start() {
  864. if (this.#animReqId === null) {
  865. this.#animReqId = window.requestAnimationFrame(this.#loop.bind(this))
  866. }
  867. }
  868.  
  869. /****************************************
  870. * コメント流しを停止
  871. */
  872. stop() {
  873. if (this.#animReqId !== null) {
  874. window.cancelAnimationFrame(this.#animReqId)
  875. this.#animReqId = null
  876. }
  877. }
  878.  
  879. /****************************************
  880. * 解放(初期化してCanvasを削除)
  881. */
  882. dispose() {
  883. this.stop()
  884.  
  885. this.#canvas?.remove()
  886. this.#resizeObs?.disconnect()
  887.  
  888. this.#id = null
  889. this.#animReqId = null
  890. this.#canvas = null
  891. this.#context2d = null
  892. this.#comments = null
  893. this.#style = null
  894. this.#option = null
  895. this.#resizeObs = null
  896.  
  897. Object.keys(this).forEach(k => delete this[k])
  898. }
  899. }

QingJ © 2025

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