ABEMA ニコニコ風コメント

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

// ==UserScript==
// @name         ABEMA ニコニコ風コメント
// @namespace    https://midra.me
// @version      1.0.3
// @description  ABEMAのコメントをニコニコ風に流すやつ
// @author       Midra
// @license      MIT
// @match        https://abema.tv/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=abema.tv
// @run-at       document-end
// @noframes
// @grant        unsafeWindow
// @grant        GM_addStyle
// @grant        GM.addStyle
// @grant        GM_getValue
// @grant        GM.getValue
// @grant        GM_setValue
// @grant        GM.setValue
// @grant        GM_deleteValue
// @grant        GM.deleteValue
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @connect      abema.tv
// ==/UserScript==

"use strict";
(() => {
  var __defProp = Object.defineProperty;
  var __export = (target, all) => {
    for (var name in all)
      __defProp(target, name, { get: all[name], enumerable: true });
  };
  var __accessCheck = (obj, member, msg) => {
    if (!member.has(obj))
      throw TypeError("Cannot " + msg);
  };
  var __privateGet = (obj, member, getter) => {
    __accessCheck(obj, member, "read from private field");
    return getter ? getter.call(obj) : member.get(obj);
  };
  var __privateAdd = (obj, member, value) => {
    if (member.has(obj))
      throw TypeError("Cannot add the same private member more than once");
    member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
  };
  var __privateSet = (obj, member, value, setter) => {
    __accessCheck(obj, member, "write to private field");
    setter ? setter.call(obj, value) : member.set(obj, value);
    return value;
  };
  var __privateWrapper = (obj, member, setter, getter) => ({
    set _(value) {
      __privateSet(obj, member, value, setter);
    },
    get _() {
      return __privateGet(obj, member, getter);
    }
  });
  var __privateMethod = (obj, member, method) => {
    __accessCheck(obj, member, "access private method");
    return method;
  };

  // ../../Library/FlowComments/src/constants.ts
  var CONFIG = {
    FONT_FAMILY: [
      "Arial",
      '"\uFF2D\uFF33 \uFF30\u30B4\u30B7\u30C3\u30AF"',
      "MS PGothic",
      '"\u30D2\u30E9\u30AE\u30CE\u89D2\u30B4\u30B7\u30C3\u30AF"',
      '"Hiragino Sans"',
      "Gulim",
      '"Malgun Gothic"',
      '"\u9ED1\u4F53"',
      "SimHei",
      "system-ui",
      "-apple-system",
      "sans-serif"
    ].join(),
    FONT_WEIGHT: "600",
    FONT_SCALE: 0.7,
    FONT_OFFSET_Y: 0.15,
    TEXT_COLOR: "#fff",
    TEXT_SHADOW_COLOR: "#000",
    TEXT_SHADOW_BLUR: 1,
    TEXT_MARGIN: 0.2,
    CANVAS_CLASSNAME: "mid-FlowComments",
    CANVAS_RATIO: 16 / 9,
    CANVAS_RESOLUTION: 720,
    RESOLUTION_LIST: [240, 360, 480, 720],
    CMT_DISPLAY_DURATION: 6e3,
    CMT_LIMIT: 0,
    LINES: 11,
    AUTO_RESIZE: true,
    AUTO_RESOLUTION: true
  };
  var ITEM_DEFAULT_OPTION = {
    position: 0 /* FLOW */,
    duration: CONFIG.CMT_DISPLAY_DURATION
  };
  var DEFAULT_OPTION = {
    resolution: CONFIG.CANVAS_RESOLUTION,
    lines: CONFIG.LINES,
    limit: CONFIG.CMT_LIMIT,
    autoResize: CONFIG.AUTO_RESIZE,
    autoResolution: CONFIG.AUTO_RESOLUTION,
    smoothRender: false
  };
  var DEFAULT_STYLE = {
    fontFamily: CONFIG.FONT_FAMILY,
    fontWeight: CONFIG.FONT_WEIGHT,
    fontScale: 1,
    color: CONFIG.TEXT_COLOR,
    shadowColor: CONFIG.TEXT_SHADOW_COLOR,
    shadowBlur: CONFIG.TEXT_SHADOW_BLUR,
    opacity: 1
  };

  // ../../Library/FlowComments/src/modules/core.ts
  var core_exports = {};
  __export(core_exports, {
    Image: () => Image2,
    Item: () => Item,
    Main: () => Main,
    Util: () => Util
  });

  // ../../Library/FlowComments/src/modules/util.ts
  var Util = class {
    static filterObject(obj) {
      if (obj !== void 0 && obj !== null && typeof obj === "object" && !Array.isArray(obj)) {
        Object.keys(obj).forEach((key) => {
          if (obj[key] === void 0 || obj[key] === null) {
            delete obj[key];
          } else {
            this.filterObject(obj[key]);
          }
        });
      }
    }
    static setStyleToCanvas(ctx, style, fontSize) {
      ctx.textBaseline = "middle";
      ctx.lineJoin = "round";
      ctx.font = `${style.fontWeight} ${fontSize * style.fontScale}px ${style.fontFamily}`;
      ctx.fillStyle = style.color;
      ctx.shadowColor = style.shadowColor;
      ctx.shadowBlur = fontSize / 16 * style.shadowBlur;
      ctx.globalAlpha = style.opacity;
    }
  };

  // ../../Library/FlowComments/src/modules/imageCache.ts
  var _OPTION, _cache;
  var ImageCache = class {
    static add(url, img) {
      if (__privateGet(this, _OPTION).maxSize < Object.keys(__privateGet(this, _cache)).length) {
        let delCacheUrl;
        Object.keys(__privateGet(this, _cache)).forEach((key) => {
          if (delCacheUrl === void 0 || __privateGet(this, _cache)[key].lastUsed < __privateGet(this, _cache)[delCacheUrl].lastUsed) {
            delCacheUrl = key;
          }
        });
        this.dispose(delCacheUrl);
      }
      __privateGet(this, _cache)[url] = {
        img,
        lastUsed: Date.now()
      };
    }
    static has(url) {
      return url !== void 0 && __privateGet(this, _cache).hasOwnProperty(url);
    }
    static async get(url) {
      return new Promise(async (resolve, reject) => {
        if (this.has(url)) {
          __privateGet(this, _cache)[url].lastUsed = Date.now();
          resolve(__privateGet(this, _cache)[url].img);
        } else {
          try {
            let img = new Image();
            img.addEventListener("load", ({ target }) => {
              if (target instanceof HTMLImageElement) {
                this.add(target.src, target);
                resolve(__privateGet(this, _cache)[target.src].img);
              } else {
                reject();
              }
            });
            img.addEventListener("error", reject);
            img.src = url;
            img = null;
          } catch (e) {
            reject(e);
          }
        }
      });
    }
    static dispose(url) {
      if (url !== void 0 && this.has(url)) {
        __privateGet(this, _cache)[url].img.remove();
        delete __privateGet(this, _cache)[url];
      }
    }
  };
  _OPTION = new WeakMap();
  _cache = new WeakMap();
  __privateAdd(ImageCache, _OPTION, {
    maxSize: 50
  });
  __privateAdd(ImageCache, _cache, {});

  // ../../Library/FlowComments/src/modules/image.ts
  var Image2 = class {
    constructor(url, alt) {
      this._url = url;
      this._alt = alt || "";
    }
    get url() {
      return this._url;
    }
    get alt() {
      return this._alt;
    }
    async get() {
      try {
        return await ImageCache.get(this._url);
      } catch (e) {
        return this._alt;
      }
    }
  };

  // ../../Library/FlowComments/src/modules/item.ts
  var Item = class {
    constructor(id, content, option, style) {
      this.position = {
        x: 0,
        y: 0,
        xp: 0,
        offsetY: 0
      };
      this.size = {
        width: 0,
        height: 0
      };
      this.scrollWidth = 0;
      this.line = 0;
      Util.filterObject(option);
      Util.filterObject(style);
      this._id = id;
      this._content = Array.isArray(content) ? content.filter((v) => v) : content;
      this._option = { ...ITEM_DEFAULT_OPTION, ...option };
      if (this._option.position === 0 /* FLOW */) {
        this._actualDuration = this._option.duration * 1.5;
      } else {
        this._actualDuration = this._option.duration;
      }
      this._style = style;
      this._canvas = document.createElement("canvas");
    }
    get id() {
      return this._id;
    }
    get content() {
      return this._content;
    }
    get style() {
      return this._style;
    }
    get option() {
      return this._option;
    }
    get actualDuration() {
      return this._actualDuration;
    }
    get canvas() {
      return this._canvas;
    }
    get top() {
      return this.position?.y || 0;
    }
    get bottom() {
      return this.position !== void 0 && this.size !== void 0 ? this.position.y + this.size.height : 0;
    }
    get left() {
      return this.position?.x || 0;
    }
    get right() {
      return this.position !== void 0 && this.size !== void 0 ? this.position.x + this.size.width : 0;
    }
    get rect() {
      return {
        width: this.size?.width || 0,
        height: this.size?.height || 0,
        top: this.top,
        bottom: this.bottom,
        left: this.left,
        right: this.right
      };
    }
    dispose() {
      this._canvas?.remove();
    }
  };

  // ../../Library/FlowComments/src/modules/main.ts
  var _id_cnt, _updateCommentsStyle, updateCommentsStyle_fn, _floor, floor_fn, _initializeComment, initializeComment_fn, _renderComment, renderComment_fn, _update, update_fn, _loop, loop_fn;
  var _Main = class {
    constructor(option, style) {
      __privateAdd(this, _updateCommentsStyle);
      __privateAdd(this, _floor);
      __privateAdd(this, _initializeComment);
      __privateAdd(this, _renderComment);
      __privateAdd(this, _update);
      __privateAdd(this, _loop);
      this.initialize(option, style);
    }
    get id() {
      return this._id;
    }
    get style() {
      return { ...DEFAULT_STYLE, ...this._style };
    }
    get option() {
      return { ...DEFAULT_OPTION, ...this._option };
    }
    get canvas() {
      return this._canvas;
    }
    get context2d() {
      return this._context2d;
    }
    get comments() {
      return this._comments;
    }
    get lineHeight() {
      return this._canvas instanceof HTMLCanvasElement ? this._canvas.height / this.option.lines : 0;
    }
    get fontSize() {
      return this.lineHeight * CONFIG.FONT_SCALE;
    }
    get isStarted() {
      return this._animReqId !== void 0;
    }
    initialize(option, style) {
      this.dispose();
      this._id = ++__privateWrapper(_Main, _id_cnt)._;
      this._canvas = document.createElement("canvas");
      this._canvas.classList.add(CONFIG.CANVAS_CLASSNAME);
      this._canvas.dataset.fcid = this._id.toString();
      this._context2d = this._canvas.getContext("2d");
      this._comments = [];
      this._resizeObs = new ResizeObserver((entries) => {
        entries.forEach((entry) => {
          if (this._canvas === void 0)
            return;
          const { width, height } = entry.contentRect;
          if (this.option.autoResize) {
            const rect_before = this._canvas.width / this._canvas.height;
            const rect_resized = width / height;
            if (0.01 < Math.abs(rect_before - rect_resized)) {
              this.resizeCanvas();
            }
          }
          if (this.option.autoResolution) {
            const resolution = CONFIG.RESOLUTION_LIST.find((v) => height <= v);
            if (Number.isFinite(resolution) && this.option.resolution !== resolution) {
              this.changeOption({ resolution });
            }
          }
        });
      });
      this._resizeObs.observe(this._canvas);
      this.changeOption(option);
      this.changeStyle(style);
    }
    changeOption(option) {
      Util.filterObject(option);
      this._option = { ...this._option, ...option };
      if (option !== void 0 && option !== null) {
        this.resizeCanvas();
      }
    }
    changeStyle(style) {
      Util.filterObject(style);
      this._style = { ...this._style, ...style };
      if (style !== void 0 && style !== null) {
        __privateMethod(this, _updateCommentsStyle, updateCommentsStyle_fn).call(this);
      }
    }
    resizeCanvas() {
      const { width, height } = this._canvas.getBoundingClientRect();
      const { resolution } = this.option;
      const ratio = width === 0 && height === 0 ? CONFIG.CANVAS_RATIO : width / height;
      this._canvas.width = resolution * ratio;
      this._canvas.height = resolution;
      __privateMethod(this, _updateCommentsStyle, updateCommentsStyle_fn).call(this);
    }
    resetCanvasStyle() {
      this.changeStyle(DEFAULT_STYLE);
    }
    async pushComment(comment) {
      if (this.isStarted === false || document.visibilityState === "hidden")
        return;
      if (0 < this.option.limit && this.option.limit <= this._comments.length) {
        this._comments.splice(0, this._comments.length - this.option.limit)[0];
      }
      await __privateMethod(this, _initializeComment, initializeComment_fn).call(this, comment);
      const spd_pushCmt = comment.scrollWidth / comment.option.duration;
      const lines_over = [...Array(this.option.lines)].map((_, i) => [i, 0]);
      this._comments.forEach((cmt) => {
        const leftTime = cmt.option.duration * (1 - cmt.position.xp);
        const isOver = comment.left - spd_pushCmt * leftTime <= 0 || comment.left <= cmt.right;
        if (isOver && cmt.line < this.option.lines) {
          lines_over[cmt.line][1]++;
        }
      });
      const lines_sort = lines_over.sort(([, cntA], [, cntB]) => cntA - cntB);
      comment.line = lines_sort[0][0];
      comment.position.y = this.lineHeight * comment.line;
      this._comments.push(comment);
    }
    start() {
      if (this._animReqId === void 0) {
        this._animReqId = window.requestAnimationFrame(__privateMethod(this, _loop, loop_fn).bind(this));
      }
    }
    stop() {
      if (this._animReqId !== void 0) {
        window.cancelAnimationFrame(this._animReqId);
        delete this._animReqId;
      }
    }
    dispose() {
      this.stop();
      this._canvas?.remove();
      this._resizeObs?.disconnect();
    }
  };
  var Main = _Main;
  _id_cnt = new WeakMap();
  _updateCommentsStyle = new WeakSet();
  updateCommentsStyle_fn = function() {
    this._context2d?.clearRect(0, 0, this._canvas.width, this._canvas.height);
    this._comments.forEach((cmt) => {
      __privateMethod(this, _initializeComment, initializeComment_fn).call(this, cmt);
      __privateMethod(this, _renderComment, renderComment_fn).call(this, cmt);
    });
  };
  _floor = new WeakSet();
  floor_fn = function(num) {
    return this._option?.smoothRender ? num : num | 0;
  };
  _initializeComment = new WeakSet();
  initializeComment_fn = async function(comment) {
    const ctx = comment.canvas.getContext("2d");
    if (ctx === null)
      return;
    ctx.clearRect(0, 0, comment.canvas.width, comment.canvas.height);
    const style = { ...this.style, ...comment.style };
    const drawFontSize = this.fontSize * style.fontScale;
    const margin = drawFontSize * CONFIG.TEXT_MARGIN;
    Util.setStyleToCanvas(ctx, style, this.fontSize);
    const aryWidth = [];
    for (const cont of comment.content) {
      if (typeof cont === "string") {
        aryWidth.push(ctx.measureText(cont).width);
      } else if (cont instanceof Image2) {
        const img = await cont.get();
        if (img instanceof HTMLImageElement) {
          const ratio = img.width / img.height;
          aryWidth.push(drawFontSize * ratio);
        } else if (img !== void 0) {
          aryWidth.push(ctx.measureText(img).width);
        } else {
          aryWidth.push(1);
        }
      }
    }
    comment.size.width = aryWidth.reduce((a, b) => a + b);
    comment.size.width += margin * (aryWidth.length - 1);
    comment.size.height = this.lineHeight;
    comment.scrollWidth = this._canvas.width + comment.size.width;
    comment.position.x = this._canvas.width - comment.scrollWidth * comment.position.xp;
    comment.position.y = this.lineHeight * comment.line;
    comment.position.offsetY = this.lineHeight / 2 * (1 + CONFIG.FONT_OFFSET_Y);
    comment.canvas.width = comment.size.width;
    comment.canvas.height = comment.size.height;
    Util.setStyleToCanvas(ctx, style, this.fontSize);
    let dx = 0;
    for (let idx = 0; idx < comment.content.length; idx++) {
      if (0 < idx) {
        dx += margin;
      }
      const cont = comment.content[idx];
      if (typeof cont === "string") {
        ctx.fillText(
          cont,
          __privateMethod(this, _floor, floor_fn).call(this, dx),
          __privateMethod(this, _floor, floor_fn).call(this, comment.position.offsetY)
        );
      } else if (cont instanceof Image2) {
        const img = await cont.get();
        if (img instanceof HTMLImageElement) {
          ctx.drawImage(
            img,
            __privateMethod(this, _floor, floor_fn).call(this, dx),
            __privateMethod(this, _floor, floor_fn).call(this, (comment.size.height - drawFontSize) / 2),
            __privateMethod(this, _floor, floor_fn).call(this, aryWidth[idx]),
            __privateMethod(this, _floor, floor_fn).call(this, drawFontSize)
          );
        } else if (img !== void 0) {
          ctx.fillText(
            img,
            __privateMethod(this, _floor, floor_fn).call(this, dx),
            __privateMethod(this, _floor, floor_fn).call(this, comment.position.offsetY)
          );
        } else {
          ctx.fillText(
            "",
            __privateMethod(this, _floor, floor_fn).call(this, dx),
            __privateMethod(this, _floor, floor_fn).call(this, comment.position.offsetY)
          );
        }
      }
      dx += aryWidth[idx];
    }
  };
  _renderComment = new WeakSet();
  renderComment_fn = function(comment) {
    this._context2d?.drawImage(
      comment.canvas,
      __privateMethod(this, _floor, floor_fn).call(this, comment.position.x),
      __privateMethod(this, _floor, floor_fn).call(this, comment.position.y)
    );
  };
  _update = new WeakSet();
  update_fn = function(time) {
    this._context2d?.clearRect(0, 0, this._canvas.width, this._canvas.height);
    this._comments.forEach((cmt, idx, ary) => {
      if (cmt.startTime === void 0) {
        cmt.startTime = time;
      }
      const elapsedTime = time - cmt.startTime;
      if (elapsedTime <= cmt.actualDuration) {
        if (cmt.option.position === 0 /* FLOW */) {
          cmt.position.xp = elapsedTime / cmt.option.duration;
          cmt.position.x = this._canvas.width - cmt.scrollWidth * cmt.position.xp;
        }
        __privateMethod(this, _renderComment, renderComment_fn).call(this, cmt);
      } else {
        cmt.dispose();
        ary.splice(idx, 1)[0];
      }
    });
  };
  _loop = new WeakSet();
  loop_fn = function(time) {
    __privateMethod(this, _update, update_fn).call(this, time);
    if (this._animReqId !== void 0) {
      this._animReqId = window.requestAnimationFrame(__privateMethod(this, _loop, loop_fn).bind(this));
    }
  };
  __privateAdd(Main, _id_cnt, 0);

  // src/constants.ts
  var STYLE = `
.mid-FlowComments {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
`;

  // src/utils/getDataFromElement.ts
  var getDataFromElement = (element) => {
    const text = element.querySelector(".com-tv-CommentBlock__message, .com-comment-CommentItem__body")?.textContent?.trim();
    const datetime = Number(element.querySelector(".com-tv-CommentBlock__time")?.getAttribute("datetime"));
    const result = {};
    if (typeof text === "string") {
      result["text"] = text;
    }
    if (Number.isFinite(datetime)) {
      result["date"] = new Date(datetime);
    }
    return result;
  };
  var getDataFromElement_default = getDataFromElement;

  // src/utils/injectStyle.ts
  var injectStyle = (css) => {
    const style = document.createElement("style");
    style.textContent = css;
    document.head.appendChild(style);
  };
  var injectStyle_default = injectStyle;

  // src/index.ts
  (async () => {
    injectStyle_default(STYLE);
    let fc = null;
    let timeoutIds = [];
    const obs_opt = {
      childList: true,
      subtree: true
    };
    const obs = new MutationObserver((mutationRecord) => {
      const comments = [];
      for (const { addedNodes, removedNodes } of mutationRecord) {
        for (const added of addedNodes) {
          if (!(added instanceof HTMLElement))
            continue;
          if (added.classList.contains("com-tv-CommentBlock--new") || added.classList.contains("com-comment-CommentItem")) {
            const data = getDataFromElement_default(added);
            if (data !== void 0) {
              comments.push(data);
            }
          }
        }
        for (const removed of removedNodes) {
          if (!(removed instanceof HTMLElement))
            continue;
          if (removed.classList.contains("com-a-Video__video") || removed.classList.contains("com-vod-VODResponsiveMainContent")) {
            timeoutIds.forEach((id) => clearTimeout(id));
            timeoutIds = [];
            fc?.dispose();
            fc = null;
          }
        }
      }
      if (0 < comments.length) {
        if (fc === null) {
          const video = document.querySelector(".com-a-Video__video, .com-live-event__LiveEventPlayerView");
          if (video instanceof HTMLElement) {
            fc = new core_exports.Main({
              autoResize: true,
              autoResolution: false,
              lines: 12,
              resolution: 720,
              smoothRender: false
            });
            video.insertAdjacentElement("afterend", fc.canvas);
            fc.start();
          }
        }
        if (comments.length === 1) {
          if (typeof comments[0].text === "string") {
            fc.pushComment(new core_exports.Item(Symbol(), [comments[0].text]));
          }
        } else {
          const cmtTimeA = comments[0].date?.getTime();
          const cmtTimeB = comments[comments.length - 1].date?.getTime();
          const diff = Number.isFinite(cmtTimeA) && Number.isFinite(cmtTimeB) ? cmtTimeB - cmtTimeA : null;
          comments.forEach((comment, idx) => {
            let timeout = 0;
            if (diff !== null) {
              timeout = (comment.date.getTime() - cmtTimeA) / diff * 8e3;
            } else {
              timeout = idx / comments.length * 8e3;
            }
            timeoutIds.push(
              setTimeout((text) => {
                fc?.pushComment(new core_exports.Item(Symbol(), [text]));
              }, timeout, comment.text)
            );
          });
        }
      }
    });
    obs.observe(document.body, obs_opt);
  })();
})();