// ==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);
})();
})();