FlowComments

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

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

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

  1. // ==UserScript==
  2. // @name FlowComments
  3. // @namespace https://midra.me
  4. // @version 1.0.4
  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.width = 0
  444. this.#canvas.height = 0
  445. this.#canvas.remove()
  446.  
  447. this.#id = null
  448. this.#content = null
  449. this.#style = null
  450. this.#option = null
  451. this.#actualDuration = null
  452. this.#canvas = null
  453.  
  454. Object.keys(this).forEach(k => delete this[k])
  455. }
  456. }
  457.  
  458. /****************************************
  459. * @classdesc コメントを流すやつ
  460. * @example
  461. * // 準備
  462. * const fc = new FlowComments()
  463. * document.body.appendChild(fc.canvas)
  464. * fc.start()
  465. *
  466. * // コメントを流す(追加する)
  467. * fc.pushComment(new FlowCommentsItem(Symbol(), 'Hello world!'))
  468. */
  469. class FlowComments {
  470. /**
  471. * インスタンスに割り当てられるIDのカウント用
  472. * @type {number}
  473. */
  474. static #id_cnt = 0
  475.  
  476. /**
  477. * インスタンスに割り当てられるID
  478. * @type {number}
  479. */
  480. #id
  481. /**
  482. * `requestAnimationFrame`の`requestID`
  483. * @type {number}
  484. */
  485. #animReqId = null
  486. /**
  487. * Canvas
  488. * @type {HTMLCanvasElement}
  489. */
  490. #canvas
  491. /**
  492. * CanvasRenderingContext2D
  493. * @type {CanvasRenderingContext2D}
  494. */
  495. #context2d
  496. /**
  497. * 現在表示中のコメント
  498. * @type {Array<FlowCommentsItem>}
  499. */
  500. #comments
  501. /**
  502. * スタイル
  503. * @type {FlowCommentsStyle}
  504. */
  505. #style
  506. /**
  507. * オプション
  508. * @type {FlowCommentsOption}
  509. */
  510. #option
  511. /**
  512. * @type {ResizeObserver}
  513. */
  514. #resizeObs
  515.  
  516. /****************************************
  517. * コンストラクタ
  518. * @param {FlowCommentsOption} [option] オプション
  519. * @param {FlowCommentsStyle} [style] スタイル
  520. */
  521. constructor(option, style) {
  522. // ID割り当て
  523. this.#id = ++FlowComments.#id_cnt
  524.  
  525. // Canvas生成
  526. this.#canvas = document.createElement('canvas')
  527. this.#canvas.classList.add(FLOWCMT_CONFIG.CANVAS_CLASSNAME)
  528. this.#canvas.dataset.fcid = this.#id.toString()
  529.  
  530. // CanvasRenderingContext2D
  531. this.#context2d = this.#canvas.getContext('2d')
  532.  
  533. // サイズ変更を監視
  534. this.#resizeObs = new ResizeObserver(entries => {
  535. const { width, height } = entries[0].contentRect
  536.  
  537. // Canvasのサイズ(比率)を自動で調整
  538. if (this.option.autoResize) {
  539. const rect_before = this.#canvas.width / this.#canvas.height
  540. const rect_resized = width / height
  541. if (0.01 < Math.abs(rect_before - rect_resized)) {
  542. this.#resizeCanvas()
  543. }
  544. }
  545.  
  546. // Canvasの解像度を自動で調整
  547. if (this.option.autoResolution) {
  548. const resolution = FLOWCMT_CONFIG.RESOLUTION_LIST.find(v => height <= v)
  549. if (Number.isFinite(resolution) && this.option.resolution !== resolution) {
  550. this.changeOption({ resolution: resolution })
  551. }
  552. }
  553. })
  554. this.#resizeObs.observe(this.#canvas)
  555.  
  556. // 初期化
  557. this.initialize(option, style)
  558. }
  559.  
  560. get id() { return this.#id }
  561. get style() { return { ...FLOWCMT_DEFAULT_STYLE, ...this.#style } }
  562. get option() { return { ...FLOWCMT_DEFAULT_OPTION, ...this.#option } }
  563. get canvas() { return this.#canvas }
  564. get context2d() { return this.#context2d }
  565. get comments() { return this.#comments }
  566.  
  567. get lineHeight() { return this.#canvas.height / this.option.lines }
  568. get fontSize() { return this.lineHeight * FLOWCMT_CONFIG.FONT_SCALE }
  569.  
  570. get isStarted() { return this.#animReqId !== null }
  571.  
  572. /****************************************
  573. * 初期化(インスタンス生成時には不要)
  574. * @param {FlowCommentsOption} [option] オプション
  575. * @param {FlowCommentsStyle} [style] スタイル
  576. */
  577. initialize(option, style) {
  578. this.stop()
  579. this.#comments = []
  580. this.changeOption(option)
  581. this.changeStyle(style)
  582. }
  583.  
  584. /****************************************
  585. * オプションを変更
  586. * @param {FlowCommentsOption} option オプション
  587. */
  588. changeOption(option) {
  589. FlowCommentsUtil.filterObject(option)
  590. this.#option = { ...this.#option, ...option }
  591. if (option !== undefined && option !== null) {
  592. this.#resizeCanvas()
  593. }
  594. }
  595.  
  596. /****************************************
  597. * スタイルを変更
  598. * @param {FlowCommentsStyle} [style] スタイル
  599. */
  600. changeStyle(style) {
  601. FlowCommentsUtil.filterObject(style)
  602. this.#style = { ...this.#style, ...style }
  603. if (style !== undefined && style !== null) {
  604. this.#updateCanvasStyle()
  605. }
  606. }
  607.  
  608. /****************************************
  609. * Canvasをリサイズ
  610. */
  611. #resizeCanvas() {
  612. // Canvasをリサイズ
  613. const { width, height } = this.#canvas.getBoundingClientRect()
  614. const { resolution } = this.option
  615. const ratio = (width === 0 && height === 0) ? FLOWCMT_CONFIG.CANVAS_RATIO : (width / height)
  616. this.#canvas.width = resolution * ratio
  617. this.#canvas.height = resolution
  618.  
  619. // Canvasのスタイルをリセット
  620. this.#updateCanvasStyle()
  621. }
  622.  
  623. /****************************************
  624. * Canvasのスタイルを更新
  625. */
  626. #updateCanvasStyle() {
  627. // スタイルを適用
  628. FlowCommentsUtil.setStyleToCanvas(
  629. this.#context2d, this.style, this.option.resolution, this.fontSize
  630. )
  631.  
  632. // Canvasをリセット
  633. this.#context2d.clearRect(0, 0, this.#canvas.width, this.#canvas.height)
  634. // コメントの各プロパティを再計算・描画
  635. this.#comments.forEach(cmt => {
  636. this.#generateCommentsItemCanvas(cmt)
  637. this.#renderComment(cmt)
  638. })
  639. }
  640.  
  641. /****************************************
  642. * Canvasのスタイルをリセット
  643. */
  644. resetCanvasStyle() {
  645. this.changeStyle(FLOWCMT_DEFAULT_STYLE)
  646. }
  647.  
  648. /****************************************
  649. * コメントの単体のCanvasを生成
  650. * @param {FlowCommentsItem} comment コメント
  651. */
  652. async #generateCommentsItemCanvas(comment) {
  653. const ctx = comment.canvas.getContext('2d')
  654. ctx.clearRect(0, 0, comment.canvas.width, comment.canvas.height)
  655.  
  656. const style = { ...this.style, ...comment.style }
  657. const drawFontSize = this.fontSize * style.fontScale
  658. const margin = drawFontSize * FLOWCMT_CONFIG.TEXT_MARGIN
  659.  
  660. // スタイルを適用
  661. FlowCommentsUtil.setStyleToCanvas(
  662. ctx, style, this.option.resolution, this.fontSize
  663. )
  664.  
  665. /** @type {Array<number>} */
  666. const aryWidth = []
  667.  
  668. //----------------------------------------
  669. // サイズを計算
  670. //----------------------------------------
  671. for (const cont of comment.content) {
  672. // 文字列
  673. if (typeof cont === 'string') {
  674. aryWidth.push(ctx.measureText(cont).width)
  675. }
  676. // 画像
  677. else if (cont instanceof FlowCommentsImage) {
  678. const img = await cont.get()
  679. if (img instanceof HTMLImageElement) {
  680. const ratio = img.width / img.height
  681. aryWidth.push(drawFontSize * ratio)
  682. } else if (img !== undefined) {
  683. aryWidth.push(ctx.measureText(img).width)
  684. } else {
  685. aryWidth.push(1)
  686. }
  687. }
  688. }
  689.  
  690. // コメントの各プロパティを計算
  691. comment.size.width = aryWidth.reduce((a, b) => a + b)
  692. comment.size.width += margin * (aryWidth.length - 1)
  693. comment.size.height = this.lineHeight
  694. comment.scrollWidth = this.#canvas.width + comment.size.width
  695. comment.position.x = this.#canvas.width - comment.scrollWidth * comment.position.xp
  696. comment.position.y = this.lineHeight * comment.line
  697. comment.position.offsetY = this.lineHeight / 2 * (1 + FLOWCMT_CONFIG.FONT_OFFSET_Y)
  698.  
  699. // Canvasのサイズを設定
  700. comment.canvas.width = comment.size.width
  701. comment.canvas.height = comment.size.height
  702.  
  703. // スタイルを再適用(上でリセットされる)
  704. FlowCommentsUtil.setStyleToCanvas(
  705. ctx, style, this.option.resolution, this.fontSize
  706. )
  707.  
  708. //----------------------------------------
  709. // コメントを描画
  710. //----------------------------------------
  711. let dx = 0
  712. for (let idx = 0; idx < comment.content.length; idx++) {
  713. if (0 < idx) {
  714. dx += margin
  715. }
  716. const cont = comment.content[idx]
  717. // 文字列
  718. if (typeof cont === 'string') {
  719. ctx.fillText(
  720. cont,
  721. ~~(dx), ~~(comment.position.offsetY)
  722. )
  723. }
  724. // 画像
  725. else if (cont instanceof FlowCommentsImage) {
  726. const img = await cont.get()
  727. if (img instanceof HTMLImageElement) {
  728. ctx.drawImage(
  729. img,
  730. ~~(dx), ~~((comment.size.height - drawFontSize) / 2),
  731. ~~(aryWidth[idx]), ~~(drawFontSize)
  732. )
  733. } else if (img !== undefined) {
  734. ctx.fillText(
  735. img,
  736. ~~(dx), ~~(comment.position.offsetY)
  737. )
  738. } else {
  739. ctx.fillText(
  740. '',
  741. ~~(dx), ~~(comment.position.offsetY)
  742. )
  743. }
  744. }
  745. dx += aryWidth[idx]
  746. }
  747. }
  748.  
  749. /****************************************
  750. * コメントを追加(流す)
  751. * @param {FlowCommentsItem} comment コメント
  752. */
  753. async pushComment(comment) {
  754. if (this.#animReqId === null || document.visibilityState === 'hidden') return
  755.  
  756. //----------------------------------------
  757. // 画面内に表示するコメントを制限
  758. //----------------------------------------
  759. if (0 < this.option.limit && this.option.limit <= this.#comments.length) {
  760. this.#comments.splice(0, this.#comments.length - this.option.limit)[0]
  761. }
  762.  
  763. //----------------------------------------
  764. // コメントの各プロパティを計算
  765. //----------------------------------------
  766. await this.#generateCommentsItemCanvas(comment)
  767.  
  768. //----------------------------------------
  769. // コメント表示行を計算
  770. //----------------------------------------
  771. const spd_pushCmt = comment.scrollWidth / comment.option.duration
  772.  
  773. // [[0, 0], [1, 0], ~ , [10, 0]] ([line, cnt])
  774. const lines_over = [...Array(this.option.lines)].map((_, i) => [i, 0])
  775.  
  776. this.#comments.forEach(cmt => {
  777. // 残り表示時間
  778. const leftTime = cmt.option.duration * (1 - cmt.position.xp)
  779. // コメント追加時に重なる or 重なる予定かどうか
  780. const isOver =
  781. comment.left - spd_pushCmt * leftTime <= 0 ||
  782. comment.left <= cmt.right
  783. if (isOver && cmt.line < this.option.lines) {
  784. lines_over[cmt.line][1]++
  785. }
  786. })
  787.  
  788. // 重なった頻度を元に昇順で並べ替える
  789. const lines_sort = lines_over.sort(([, cntA], [, cntB]) => cntA - cntB)
  790.  
  791. comment.line = lines_sort[0][0]
  792. comment.position.y = this.lineHeight * comment.line
  793.  
  794. //----------------------------------------
  795. // コメントを追加
  796. //----------------------------------------
  797. this.#comments.push(comment)
  798. }
  799.  
  800. /****************************************
  801. * テキストを描画
  802. * @param {FlowCommentsItem} comment コメント
  803. */
  804. #renderComment(comment) {
  805. this.#context2d.drawImage(
  806. comment.canvas,
  807. ~~(comment.position.x), ~~(comment.position.y)
  808. )
  809. }
  810.  
  811. /****************************************
  812. * ループ中に実行される処理
  813. * @param {number} time 時間
  814. */
  815. #update(time) {
  816. // Canvasをリセット
  817. this.#context2d.clearRect(0, 0, this.#canvas.width, this.#canvas.height)
  818.  
  819. this.#comments.forEach((cmt, idx, ary) => {
  820. // コメントを流し始めた時間
  821. if (cmt.startTime === null) {
  822. cmt.startTime = time
  823. }
  824.  
  825. // コメントを流し始めて経過した時間
  826. const elapsedTime = time - cmt.startTime
  827.  
  828. if (elapsedTime <= cmt.actualDuration) {
  829. // コメントの座標を更新(流すコメント)
  830. if (cmt.option.position === FLOWCMT_TYPE.FLOW) {
  831. cmt.position.xp = elapsedTime / cmt.option.duration
  832. cmt.position.x = this.#canvas.width - cmt.scrollWidth * cmt.position.xp
  833. }
  834. // コメントを描画
  835. this.#renderComment(cmt)
  836. } else {
  837. // 表示時間を超えたら消す
  838. cmt.dispose()
  839. ary.splice(idx, 1)[0]
  840. }
  841. })
  842. }
  843.  
  844. /****************************************
  845. * ループ処理
  846. * @param {number} time 時間
  847. */
  848. #loop(time) {
  849. this.#update(time)
  850. if (this.#animReqId !== null) {
  851. this.#animReqId = window.requestAnimationFrame(this.#loop.bind(this))
  852. }
  853. }
  854.  
  855. /****************************************
  856. * コメント流しを開始
  857. */
  858. start() {
  859. if (this.#animReqId === null) {
  860. this.#animReqId = window.requestAnimationFrame(this.#loop.bind(this))
  861. }
  862. }
  863.  
  864. /****************************************
  865. * コメント流しを停止
  866. */
  867. stop() {
  868. if (this.#animReqId !== null) {
  869. window.cancelAnimationFrame(this.#animReqId)
  870. this.#animReqId = null
  871. }
  872. }
  873.  
  874. /****************************************
  875. * 解放(初期化してCanvasを削除)
  876. */
  877. dispose() {
  878. this.initialize()
  879.  
  880. this.#canvas.width = 0
  881. this.#canvas.height = 0
  882. this.#canvas.remove()
  883. this.#resizeObs.unobserve(this.#canvas)
  884.  
  885. this.#id = null
  886. this.#animReqId = null
  887. this.#canvas = null
  888. this.#context2d = null
  889. this.#comments = null
  890. this.#style = null
  891. this.#option = null
  892. this.#resizeObs = null
  893. }
  894. }

QingJ © 2025

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