FlowComments

コメントを流すやつ

Tento skript by neměl být instalován přímo. Jedná se o knihovnu, kterou by měly jiné skripty využívat pomocí meta příkazu // @require https://update.greatest.deepsurf.us/scripts/444119/1061612/FlowComments.js

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