danmu
Version:
Flexible, cross-platform, powerful danmu library.
1,810 lines (1,802 loc) • 87.3 kB
JavaScript
/*!
* danmu.js
* (c) 2024-2025 Imtaotao
* Released under the MIT License.
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined'
? factory(exports)
: typeof define === 'function' && define.amd
? define(['exports'], factory)
: ((global =
typeof globalThis !== 'undefined' ? globalThis : global || self),
factory((global.Danmu = {})));
})(this, function (exports) {
'use strict';
class Queue {
constructor() {
this._fx = [];
this._init = true;
this._lock = false;
this._finishDefers = new Set();
}
_next() {
if (!this._lock) {
this._lock = true;
if (this._fx.length === 0) {
this._init = true;
this._finishDefers.forEach((d) => d.resolve());
this._finishDefers.clear();
} else {
const fn = this._fx.shift();
if (fn) {
fn(() => {
this._lock = false;
this._next();
});
}
}
}
}
add(fn) {
this._fx.push(fn);
if (this._init) {
this._lock = false;
this._init = false;
this._next();
}
}
awaitFinish() {
if (this._init) return Promise.resolve();
const defer = {};
this._finishDefers.add(defer);
return new Promise((resolve) => {
defer.resolve = resolve;
});
}
}
const objectToString = Object.prototype.toString;
const toRawType = (val) => {
return objectToString.call(val).slice(8, -1).toLowerCase();
};
const isArray = (() => Array.isArray)();
const isObject = (val) => {
return val !== null && typeof val === 'object';
};
const isNumber = (val) => {
return typeof val === 'number';
};
const isPromiseLike = (val) => {
return isObject(val) && typeof val.then === 'function';
};
const isPlainObject = (val) => {
return objectToString.call(val) === '[object Object]';
};
const isSet =
typeof Set !== 'function' || !(() => Set.prototype.has)()
? () => false
: (v) => isObject(v) && v instanceof Set;
const isInBounds = ([a, b], val) => {
if (val === a || val === b) return true;
const min = Math.min(a, b);
const max = min === a ? b : a;
return min < val && val < max;
};
const isEmptyObject = (val) => {
for (const _ in val) return false;
return true;
};
const isPrimitiveValue = (val) => {
return (
typeof val === 'number' ||
typeof val === 'bigint' ||
typeof val === 'string' ||
typeof val === 'symbol' ||
typeof val === 'boolean' ||
val === undefined ||
val === null
);
};
const isWhitespace = (char) => {
return (
char === ' ' ||
char === '\t' ||
char === '\n' ||
char === '\r' ||
char === '\f' ||
char === '\v'
);
};
let byteToHex;
const unsafeStringify = (arr) => {
if (!byteToHex) {
byteToHex = [];
for (let i = 0; i < 256; ++i) {
byteToHex.push((i + 0x100).toString(16).slice(1));
}
}
return (
byteToHex[arr[0]] +
byteToHex[arr[1]] +
byteToHex[arr[2]] +
byteToHex[arr[3]] +
'-' +
byteToHex[arr[4]] +
byteToHex[arr[5]] +
'-' +
byteToHex[arr[6]] +
byteToHex[arr[7]] +
'-' +
byteToHex[arr[8]] +
byteToHex[arr[9]] +
'-' +
byteToHex[arr[10]] +
byteToHex[arr[11]] +
byteToHex[arr[12]] +
byteToHex[arr[13]] +
byteToHex[arr[14]] +
byteToHex[arr[15]]
).toLowerCase();
};
let poolPtr;
let rnds8Pool;
const rng = () => {
if (!rnds8Pool) {
rnds8Pool = new Uint8Array(256);
poolPtr = rnds8Pool.length;
}
if (poolPtr > 256 - 16) {
for (let i = 0; i < 256; i++) {
rnds8Pool[i] = Math.floor(Math.random() * 256);
}
poolPtr = 0;
}
return rnds8Pool.slice(poolPtr, (poolPtr += 16));
};
const uuid = () => {
const rnds = rng();
rnds[6] = (rnds[6] & 0x0f) | 0x40;
rnds[8] = (rnds[8] & 0x3f) | 0x80;
return unsafeStringify(rnds);
};
const loopSlice = (l, fn, taskTime = 17) => {
return new Promise((resolve) => {
if (l === 0) {
resolve();
return;
}
let i = -1;
let start = Date.now();
const run = () => {
while (++i < l) {
if (fn(i) === false) {
resolve();
break;
}
if (i === l - 1) {
resolve();
} else {
const t = Date.now();
if (t - start > taskTime) {
start = t;
raf(run);
break;
}
}
}
};
run();
});
};
class Calculator {
expr;
i = 0;
priority = {
'+': 1,
'-': 1,
'*': 2,
'/': 2,
'%': 2,
};
constructor(expr) {
this.expr = expr;
}
calculateOperation(numbers, operator) {
const b = numbers.pop();
const a = numbers.pop();
if (a !== undefined && b !== undefined) {
switch (operator) {
case '+':
numbers.push(a + b);
break;
case '-':
numbers.push(a - b);
break;
case '*':
numbers.push(a * b);
break;
case '/':
numbers.push(a / b);
break;
case '%':
numbers.push(a % b);
break;
default:
throw new Error(`Invalid operator: "${operator}"`);
}
}
}
evaluate(tokens) {
if (tokens.length === 0) return NaN;
const numbers = [];
const operators = [];
for (const token of tokens) {
if (typeof token === 'string') {
const cur = this.priority[token];
while (
operators.length > 0 &&
this.priority[last(operators)] >= cur
) {
this.calculateOperation(numbers, operators.pop());
}
operators.push(token);
} else {
numbers.push(token);
}
}
while (operators.length > 0) {
this.calculateOperation(numbers, operators.pop());
}
const n = numbers.pop();
return typeof n === 'number' ? n : NaN;
}
tokenizer() {
const tokens = [];
if (!this.expr) return tokens;
let buf = '';
const add = () => {
if (buf) {
tokens.push(Number(buf));
buf = '';
}
};
for (; this.i < this.expr.length; this.i++) {
const char = this.expr[this.i];
if (isWhitespace(char));
else if (char === '+' || char === '-') {
const prevToken = last(tokens);
if (!buf && (!prevToken || prevToken in this.priority)) {
buf += char;
} else {
add();
tokens.push(char);
}
} else if (char === '*' || char === '/' || char === '%') {
add();
tokens.push(char);
} else if (char === '(') {
this.i++;
tokens.push(this.evaluate(this.tokenizer()));
} else if (char === ')') {
this.i++;
add();
return tokens;
} else {
buf += char;
}
}
add();
return tokens;
}
}
const isLegalExpression = (expr) => {
const keywords = '\',",`,:,;,[,{,=,var,let,const,return'.split(',');
for (const word of keywords) {
if (expr.includes(word)) {
return false;
}
}
return !/[^\+\-\*\/\%\s]+\(/.test(expr);
};
const mathExprEvaluate = (expr, options) => {
const { units, verify, actuator, exec = true } = options || {};
if (verify && !isLegalExpression(expr)) {
throw new Error(`Invalid expression: "${expr}"`);
}
expr = expr.replace(
/(-?\d+(\.\d+)?|NaN|Infinity)([^\d\s\+\-\*\/\.\(\)]+)?/g,
($1, n, $3, u, $4) => {
if (!u) return n;
const parser = units && (units[u] || units['default']);
if (!parser) throw new Error(`Invalid unit: "${u}"`);
return String(parser(n, u, expr));
},
);
try {
if (actuator) {
return actuator(expr, Boolean(exec));
} else {
if (!exec) return expr;
const calculator = new Calculator(expr);
return calculator.evaluate(calculator.tokenizer());
}
} catch (e) {
throw new Error(`Invalid expression: "${expr}", error: "${e}"`);
}
};
function assert(condition, error) {
if (!condition) throw new Error(error);
}
const raf = (fn) => {
typeof requestAnimationFrame === 'function'
? requestAnimationFrame(fn)
: typeof process !== 'undefined' && typeof process.nextTick === 'function'
? process.nextTick(fn)
: setTimeout(fn, 17);
};
const now = () =>
typeof performance !== 'undefined' && typeof performance.now === 'function'
? performance.now()
: Date.now();
const last = (arr, i = 0) => {
return arr[arr.length + i - 1];
};
const hasOwn = (obj, key) => {
return Object.hasOwnProperty.call(obj, key);
};
const decimalPlaces = (n) =>
!Number.isFinite(n) || Number.isInteger(n)
? 0
: String(n).split('.')[1].length;
const random = (min = 0, max = 0) => {
if (max === min) return max;
if (max < min) min = [max, (max = min)][0];
const n = Number(
(Math.random() * (max - min) + min).toFixed(
Math.max(decimalPlaces(min), decimalPlaces(max)),
),
);
if (n > max) return max;
if (n < min) return min;
return n;
};
const once = (fn) => {
let result;
let called = false;
function wrap(...args) {
if (called) return result;
called = true;
result = fn.apply(this, args);
return result;
}
return wrap;
};
const remove = (arr, el) => {
if (isArray(arr)) {
const i = arr.indexOf(el);
if (i > -1) {
arr.splice(i, 1);
return true;
}
return false;
} else {
if (arr.has(el)) {
arr.delete(el);
return true;
}
return false;
}
};
function map(data, fn) {
fn = fn || ((val) => val);
if (isArray(data)) {
return data.map((val, i) => fn(val, i));
}
if (isSet(data)) {
const cloned = new Set();
for (const val of data) {
cloned.add(fn(val));
}
return cloned;
}
if (isPlainObject(data)) {
const cloned = {};
for (const key in data) {
cloned[key] = fn(data[key], key);
}
return cloned;
}
throw new Error(`Invalid type "${toRawType(data)}"`);
}
const pick = (val, keys, omitUndefined = false) => {
return keys.reduce((n, k) => {
if (k in val) {
if (!omitUndefined || val[k] !== undefined) {
n[k] = val[k];
}
}
return n;
}, {});
};
const omit = (val, keys) => {
return Object.keys(val).reduce((n, k) => {
if (!keys.includes(k)) {
n[k] = val[k];
}
return n;
}, {});
};
const deferred = () => {
let reject;
let resolve;
const promise = new Promise((rs, rj) => {
reject = rj;
resolve = rs;
});
return { promise, resolve, reject };
};
const batchProcess = ({ ms, processor }) => {
const queue = [];
const flush = () => {
setTimeout(() => {
const ls = [];
const fns = [];
for (const { value, resolve } of queue) {
ls.push(value);
fns.push(resolve);
}
queue.length = 0;
processor(ls);
for (const fn of fns) {
fn();
}
}, ms || 0);
};
return (value) => {
const defer = deferred();
if (queue.length === 0) flush();
queue.push({ value, resolve: defer.resolve });
return defer.promise;
};
};
class Track {
_container;
isLock = false;
index;
location;
list;
constructor({ index, list, location, container }) {
this.list = list;
this.index = index;
this.location = location;
this._container = container;
}
get width() {
return this._container.width;
}
get height() {
return this.location.bottom - this.location.top;
}
// We have to make a copy.
// During the loop, there are too many factors that change danmaku,
// which makes it impossible to guarantee the stability of the list.
each(fn) {
for (const dm of Array.from(this.list)) {
if (fn(dm) === false) break;
}
}
lock() {
this.isLock = true;
}
unlock() {
this.isLock = false;
}
clear() {
this.each((dm) => dm.destroy());
}
/**
* @internal
*/
_add(dm) {
this.list.push(dm);
}
/**
* @internal
*/
_remove(dm) {
remove(this.list, dm);
}
/**
* @internal
*/
_updateLocation(location) {
this.location = location;
}
/**
* @internal
*/
_last(li) {
for (let i = this.list.length - 1; i >= 0; i--) {
const dm = this.list[i - li];
if (dm && !dm.paused && dm.loops === 0 && dm.type === 'facile') {
return dm;
}
}
return null;
}
}
const INTERNAL_FLAG = Symbol();
const ids = {
danmu: 1,
bridge: 1,
runtime: 1,
container: 1,
};
const nextFrame = (fn) => raf(() => raf(fn));
const randomIdx = (founds, rows) => {
const n = Math.floor(Math.random() * rows);
return founds.has(n) ? randomIdx(founds, rows) : n;
};
const toNumber = (val, all) => {
return mathExprEvaluate(val, {
units: {
px: (n) => n,
'%': (n) => (Number(n) / 100) * all,
},
});
};
const whenTransitionEnds = (node) => {
return new Promise((resolve) => {
const onEnd = once(() => {
node.removeEventListener('transitionend', onEnd);
resolve();
});
node.addEventListener('transitionend', onEnd);
});
};
class Container {
width = 0;
height = 0;
node;
parentNode = null;
_parentWidth = 0;
_parentHeight = 0;
_size = {
x: { start: 0, end: '100%' },
y: { start: 0, end: '100%' },
};
constructor() {
this.node = document.createElement('div');
this.node.setAttribute('data-danmu-container', String(ids.container++));
this.setStyle('overflow', 'hidden');
this.setStyle('position', 'relative');
this.setStyle('top', '0');
this.setStyle('left', '0');
}
/**
* @internal
*/
_sizeToNumber() {
const size = Object.create(null);
const transform = (v, all) => {
return typeof v === 'string' ? (v ? toNumber(v, all) : 0) : v;
};
size.x = map(this._size.x, (v) => transform(v, this._parentWidth));
size.y = map(this._size.y, (v) => transform(v, this._parentHeight));
return size;
}
/**
* @internal
*/
_mount(node) {
this._unmount();
this.parentNode = node;
this._format();
this.parentNode.appendChild(this.node);
}
/**
* @internal
*/
_unmount() {
this.parentNode = null;
if (this.node.parentNode) {
this.node.parentNode.removeChild(this.node);
}
}
/**
* @internal
*/
_updateSize({ x, y }) {
const isLegal = (v) => {
return typeof v === 'string' || typeof v === 'number';
};
if (x) {
if (isLegal(x.end)) this._size.x.end = x.end;
if (isLegal(x.start)) this._size.x.start = x.start;
}
if (y) {
if (isLegal(y.end)) this._size.y.end = y.end;
if (isLegal(y.start)) this._size.y.start = y.start;
}
}
/**
* @internal
*/
_toNumber(p, val) {
let n = typeof val === 'number' ? val : toNumber(val, this[p]);
if (n > this[p]) n = this[p];
assert(!Number.isNaN(n), `${val} is not a number`);
return n;
}
/**
* @internal
*/
_format() {
if (this.parentNode) {
const styles = getComputedStyle(this.parentNode);
this._parentWidth = Number(styles.width.replace('px', ''));
this._parentHeight = Number(styles.height.replace('px', ''));
}
const { x, y } = this._sizeToNumber();
this.width = x.end - x.start;
this.height = y.end - y.start;
this.setStyle('left', `${x.start}px`);
this.setStyle('top', `${y.start}px`);
this.setStyle('width', `${this.width}px`);
this.setStyle('height', `${this.height}px`);
}
setStyle(key, val) {
this.node.style[key] = val;
}
}
const INTERNAL = Symbol('internal_hooks');
const INVALID_VALUE = Symbol('invalid_condition_value');
const PERFORMANCE_PLUGIN_PREFIX = '__performance_monitor__';
const isBrowser = typeof window !== 'undefined';
let taskId = 1;
const createTaskId = () => taskId++;
let monitorTaskId = 1;
const createMonitorTaskId = () => monitorTaskId++;
let monitorPluginId = 1;
const createMonitorPluginId = () => monitorPluginId++;
const checkReturnData = (originData, returnData) => {
if (!isPlainObject(returnData)) return false;
if (originData !== returnData) {
for (const key in originData) {
if (!(key in returnData)) {
return false;
}
}
}
return true;
};
const getTargetInArgs = (key, args) => {
let target = args;
const parts = key.split('.');
for (let i = 0, l = parts.length; i < l; i++) {
if (!target) return INVALID_VALUE;
let p = parts[i];
if (p.startsWith('[') && p.endsWith(']')) {
p = Number(p.slice(1, -1));
}
target = target[p];
}
return target;
};
class SyncHook {
constructor(context, _type = 'SyncHook', _internal) {
this.listeners = new Set();
this.tags = new WeakMap();
this.errors = new Set();
this.type = _type;
this._locked = false;
this.context = typeof context === 'undefined' ? null : context;
if (_internal !== INTERNAL) {
this.before = new SyncHook(null, 'SyncHook', INTERNAL);
this.after = new SyncHook(null, 'SyncHook', INTERNAL);
}
}
_emitError(error, hook, tag) {
if (this.errors.size > 0) {
this.errors.forEach((fn) =>
fn({
tag,
hook,
error,
type: this.type,
}),
);
} else {
throw error;
}
}
isEmpty() {
return this.listeners.size === 0;
}
lock() {
this._locked = true;
if (this.before) this.before.lock();
if (this.after) this.after.lock();
return this;
}
unlock() {
this._locked = false;
if (this.before) this.before.unlock();
if (this.after) this.after.unlock();
return this;
}
on(tag, fn) {
assert(!this._locked, 'The current hook is now locked.');
if (typeof tag === 'function') {
fn = tag;
tag = '';
}
assert(typeof fn === 'function', `Invalid parameter in "${this.type}".`);
if (tag && typeof tag === 'string') {
this.tags.set(fn, tag);
}
this.listeners.add(fn);
return this;
}
once(tag, fn) {
if (typeof tag === 'function') {
fn = tag;
tag = '';
}
const self = this;
this.on(tag, function wrapper(...args) {
self.remove(wrapper, INTERNAL);
return fn.apply(this, args);
});
return this;
}
emit(...data) {
var _a, _b, _c;
if (this.listeners.size > 0) {
const id = createTaskId();
let map = null;
if (
!((_a = this.after) === null || _a === void 0 ? void 0 : _a.isEmpty())
) {
map = Object.create(null);
}
(_b = this.before) === null || _b === void 0
? void 0
: _b.emit(id, this.type, this.context, data);
this.listeners.forEach((fn) => {
const tag = this.tags.get(fn);
if (map && tag) {
map[tag] = Date.now();
}
const record = () => {
if (map && tag) {
map[tag] = Date.now() - map[tag];
}
};
try {
fn.apply(this.context, data);
record();
} catch (e) {
record();
this._emitError(e, fn, tag);
}
});
(_c = this.after) === null || _c === void 0
? void 0
: _c.emit(id, this.type, this.context, data, map);
}
}
remove(fn, _flag) {
if (_flag !== INTERNAL) {
assert(!this._locked, 'The current hook is now locked.');
}
this.listeners.delete(fn);
return this;
}
removeAll() {
assert(!this._locked, 'The current hook is now locked.');
this.listeners.clear();
return this;
}
listenError(fn) {
assert(!this._locked, 'The current hook is now locked.');
this.errors.add(fn);
}
clone() {
return new this.constructor(
this.context,
this.type,
this.before ? null : INTERNAL,
);
}
}
class AsyncHook extends SyncHook {
constructor(context) {
super(context, 'AsyncHook');
}
emit(...data) {
var _a, _b;
let id;
let result;
const ls = Array.from(this.listeners);
let map = null;
if (ls.length > 0) {
id = createTaskId();
if (
!((_a = this.after) === null || _a === void 0 ? void 0 : _a.isEmpty())
) {
map = Object.create(null);
}
(_b = this.before) === null || _b === void 0
? void 0
: _b.emit(id, this.type, this.context, data);
let i = 0;
const call = (prev) => {
if (prev === false) {
return false;
} else if (i < ls.length) {
let res;
const fn = ls[i++];
const tag = this.tags.get(fn);
if (map && tag) {
map[tag] = Date.now();
}
const record = () => {
if (map && tag) {
map[tag] = Date.now() - map[tag];
}
};
try {
res = fn.apply(this.context, data);
} catch (e) {
record();
this._emitError(e, fn, tag);
return call(prev);
}
return Promise.resolve(res)
.finally(record)
.then(call)
.catch((e) => {
this._emitError(e, fn, tag);
return call(prev);
});
} else {
return prev;
}
};
result = call();
}
return Promise.resolve(result).then((result) => {
var _a;
if (ls.length > 0) {
(_a = this.after) === null || _a === void 0
? void 0
: _a.emit(id, this.type, this.context, data, map);
}
return result;
});
}
}
class SyncWaterfallHook extends SyncHook {
constructor(context) {
super(context, 'SyncWaterfallHook');
}
emit(data) {
var _a, _b, _c;
assert(
isPlainObject(data),
`"${this.type}" hook response data must be an object.`,
);
if (this.listeners.size > 0) {
const id = createTaskId();
let map = null;
if (
!((_a = this.after) === null || _a === void 0 ? void 0 : _a.isEmpty())
) {
map = Object.create(null);
}
(_b = this.before) === null || _b === void 0
? void 0
: _b.emit(id, this.type, this.context, [data]);
for (const fn of this.listeners) {
const tag = this.tags.get(fn);
if (map && tag) {
map[tag] = Date.now();
}
const record = () => {
if (map && tag) {
map[tag] = Date.now() - map[tag];
}
};
try {
const tempData = fn.call(this.context, data);
assert(
checkReturnData(data, tempData),
`The return value of hook "${this.type}" is incorrect.`,
);
data = tempData;
record();
} catch (e) {
record();
this._emitError(e, fn, tag);
}
}
(_c = this.after) === null || _c === void 0
? void 0
: _c.emit(id, this.type, this.context, [data], map);
}
return data;
}
}
class AsyncParallelHook extends SyncHook {
constructor(context) {
super(context, 'AsyncParallelHook');
}
emit(...data) {
var _a, _b;
let id;
let map = null;
const size = this.listeners.size;
const taskList = [];
if (size > 0) {
id = createTaskId();
if (
!((_a = this.after) === null || _a === void 0 ? void 0 : _a.isEmpty())
) {
map = Object.create(null);
}
(_b = this.before) === null || _b === void 0
? void 0
: _b.emit(id, this.type, this.context, data);
for (const fn of this.listeners) {
taskList.push(
Promise.resolve().then(() => {
const tag = this.tags.get(fn);
if (map && tag) {
map[tag] = Date.now();
}
const record = () => {
if (map && tag) {
map[tag] = Date.now() - map[tag];
}
};
try {
const res = fn.apply(this.context, data);
if (isPromiseLike(res)) {
return Promise.resolve(res).catch((e) => {
record();
this._emitError(e, fn, tag);
return null;
});
} else {
record();
return res;
}
} catch (e) {
this._emitError(e, fn, tag);
return null;
}
}),
);
}
}
return Promise.all(taskList).then(() => {
var _a;
if (size > 0) {
(_a = this.after) === null || _a === void 0
? void 0
: _a.emit(id, this.type, this.context, data, map);
}
});
}
}
class AsyncWaterfallHook extends SyncHook {
constructor(context) {
super(context, 'AsyncWaterfallHook');
}
emit(data) {
var _a, _b;
assert(
isPlainObject(data),
`"${this.type}" hook response data must be an object.`,
);
let i = 0;
let id;
let map = null;
const ls = Array.from(this.listeners);
if (ls.length > 0) {
id = createTaskId();
if (
!((_a = this.after) === null || _a === void 0 ? void 0 : _a.isEmpty())
) {
map = Object.create(null);
}
(_b = this.before) === null || _b === void 0
? void 0
: _b.emit(id, this.type, this.context, [data]);
const call = (prev) => {
if (prev === false) {
return false;
} else {
assert(
checkReturnData(data, prev),
`The return value of hook "${this.type}" is incorrect.`,
);
data = prev;
if (i < ls.length) {
let res;
const fn = ls[i++];
const tag = this.tags.get(fn);
if (map && tag) {
map[tag] = Date.now();
}
const record = () => {
if (map && tag) {
map[tag] = Date.now() - map[tag];
}
};
try {
res = fn.call(this.context, prev);
} catch (e) {
record();
this._emitError(e, fn, tag);
return call(prev);
}
return Promise.resolve(res)
.finally(record)
.then(call)
.catch((e) => {
this._emitError(e, fn, tag);
return call(prev);
});
}
}
return data;
};
return Promise.resolve(call(data)).then((data) => {
var _a;
(_a = this.after) === null || _a === void 0
? void 0
: _a.emit(id, this.type, this.context, [data], map);
return data;
});
} else {
return Promise.resolve(data);
}
}
}
function createPerformance(plSys, defaultCondition) {
let hooks = {};
let closed = false;
const pluginName = `${PERFORMANCE_PLUGIN_PREFIX}${createMonitorPluginId()}`;
let records1 = new Map();
let records2 = Object.create(null);
let monitorTask = Object.create(null);
const findCondition = (key, conditions) => {
if (!conditions) return defaultCondition;
return conditions[key] || defaultCondition;
};
for (const key in plSys.lifecycle) {
hooks[key] = function (...args) {
let value;
for (const id in monitorTask) {
const [sk, ek, conditions, hook] = monitorTask[id];
const condition = findCondition(key, conditions);
if (key === ek) {
value = getTargetInArgs(condition, args);
if (value !== INVALID_VALUE) {
const prevObj = isPrimitiveValue(value)
? records2[value]
: records1.get(value);
if (prevObj) {
const prevTime = prevObj[`${id}_${sk}`];
if (typeof prevTime === 'number') {
hook.emit({
endArgs: args,
endContext: this,
events: [sk, ek],
equalValue: value,
time: Date.now() - prevTime,
});
}
}
}
}
if (key === sk) {
value = value || getTargetInArgs(condition, args);
if (value !== INVALID_VALUE) {
let obj;
const k = `${id}_${sk}`;
const t = Date.now();
if (isPrimitiveValue(value)) {
obj = records2[value];
if (!obj) {
obj = Object.create(null);
records2[value] = obj;
}
} else {
obj = records1.get(value);
if (!obj) {
obj = Object.create(null);
records1.set(value, obj);
}
}
obj[k] = t;
}
}
}
};
}
plSys.use({
hooks,
name: pluginName,
});
return {
close() {
if (!closed) {
closed = true;
records1.clear();
records2 = Object.create(null);
monitorTask = Object.create(null);
this._taskHooks.hs.forEach((hook) => hook.removeAll());
this._taskHooks.hs.clear();
plSys.remove(pluginName);
}
},
monitor(sk, ek, conditions) {
assert(
!closed,
'Unable to add tasks to a closed performance observer.',
);
const id = createMonitorTaskId();
const hook = new SyncHook();
const task = [sk, ek, conditions, hook];
monitorTask[id] = task;
this._taskHooks.add(hook);
return hook;
},
_taskHooks: {
hs: new Set(),
watch: new Set(),
add(hook) {
this.hs.add(hook);
this.watch.forEach((fn) => fn(hook));
},
},
};
}
function logPerformance(p, performanceReceiver, tag) {
const _tag = `[${tag || 'debug'}_performance]`;
const fn = (e) => {
if (typeof performanceReceiver === 'function') {
performanceReceiver({ tag, e });
} else {
console.log(
`${_tag}(${e.events[0]} -> ${e.events[1]}): ${e.time}`,
e.endArgs,
e.endContext,
);
}
};
p._taskHooks.watch.add((hook) => hook.on(fn));
p._taskHooks.hs.forEach((hook) => hook.on(fn));
}
function createDebugger(plSys, options) {
let {
tag,
group,
filter,
receiver,
listenError,
logPluginTime,
errorReceiver,
performance,
performanceReceiver,
} = options;
let unsubscribeError = null;
let map = Object.create(null);
const _tag = `[${tag || 'debug'}]: `;
if (!('group' in options)) group = isBrowser;
if (!('listenError' in options)) listenError = true;
if (!('logPluginTime' in options)) logPluginTime = true;
if (performance) logPerformance(performance, performanceReceiver, tag);
const prefix = (e) => {
let p = `${_tag}${e.name}_${e.id}(t, args, ctx`;
p += logPluginTime ? ', pt)' : ')';
return p;
};
const unsubscribeBefore = plSys.beforeEach((e) => {
map[e.id] = { t: Date.now() };
if (typeof receiver !== 'function') {
console.time(prefix(e));
if (group) console.groupCollapsed(e.name);
}
});
const unsubscribeAfter = plSys.afterEach((e) => {
let t = null;
if (typeof filter === 'string') {
if (e.name.startsWith(filter)) {
if (group) console.groupEnd();
return;
}
} else if (typeof filter === 'function') {
t = Date.now() - map[e.id].t;
if (filter({ e, tag, time: t })) {
if (group) console.groupEnd();
return;
}
}
if (typeof receiver === 'function') {
if (t === null) {
t = Date.now() - map[e.id].t;
}
receiver({ e, tag, time: t });
} else {
console.timeLog(
prefix(e),
e.args,
e.context,
logPluginTime ? e.pluginExecTime : '',
);
if (group) console.groupEnd();
}
});
if (listenError) {
unsubscribeError = plSys.listenError((e) => {
if (typeof errorReceiver === 'function') {
errorReceiver(e);
} else {
console.error(
`[${tag}]: The error originated from "${e.tag}.${e.name}(${e.type})".\n`,
`The hook function is: ${String(e.hook)}\n\n`,
e.error,
);
}
});
}
return () => {
unsubscribeBefore();
unsubscribeAfter();
if (unsubscribeError) {
unsubscribeError();
}
map = Object.create(null);
if (performance) {
performance.close();
}
};
}
const HOOKS = {
SyncHook,
AsyncHook,
AsyncParallelHook,
SyncWaterfallHook,
AsyncWaterfallHook,
};
class PluginSystem {
constructor(lifecycle) {
this._locked = false;
this._debugs = new Set();
this._performances = new Set();
this._lockListenSet = new Set();
this.plugins = Object.create(null);
this.lifecycle = lifecycle || Object.create(null);
}
_onEmitLifeHook(type, fn) {
assert(
!this._locked,
`The plugin system is locked and cannot add "${type}" hook.`,
);
let map = Object.create(null);
for (const key in this.lifecycle) {
map[key] = (id, type, context, args, map) => {
fn(
Object.freeze({
id,
type,
args,
context,
name: key,
pluginExecTime: map,
}),
);
};
this.lifecycle[key][type].on(map[key]);
}
return () => {
for (const key in this.lifecycle) {
this.lifecycle[key][type].remove(map[key]);
}
map = Object.create(null);
};
}
listenLock(fn) {
this._lockListenSet.add(fn);
}
lock() {
this._locked = true;
for (const key in this.lifecycle) {
this.lifecycle[key].lock();
}
if (this._lockListenSet.size > 0) {
this._lockListenSet.forEach((fn) => fn(true));
}
}
unlock() {
this._locked = false;
for (const key in this.lifecycle) {
this.lifecycle[key].unlock();
}
if (this._lockListenSet.size > 0) {
this._lockListenSet.forEach((fn) => fn(false));
}
}
beforeEach(fn) {
return this._onEmitLifeHook('before', fn);
}
afterEach(fn) {
return this._onEmitLifeHook('after', fn);
}
performance(defaultCondition) {
assert(
!this._locked,
'The plugin system is locked and performance cannot be monitored.',
);
assert(
defaultCondition && typeof defaultCondition === 'string',
'A judgment `conditions` is required to use `performance`.',
);
const obj = createPerformance(this, defaultCondition);
const { close } = obj;
const fn = () => {
assert(
!this._locked,
'The plugin system is locked and removal operations are not allowed.',
);
this._performances.delete(fn);
return close.call(obj);
};
obj.close = fn;
this._performances.add(fn);
return obj;
}
removeAllPerformance() {
assert(
!this._locked,
'The plugin system is locked and removal operations are not allowed.',
);
this._performances.forEach((fn) => fn());
}
debug(options = {}) {
assert(
!this._locked,
'The plugin system is locked and the debugger cannot be added.',
);
const close = createDebugger(this, options);
const fn = () => {
assert(
!this._locked,
'The plugin system is locked and removal operations are not allowed.',
);
this._debugs.delete(fn);
close();
};
this._debugs.add(fn);
return fn;
}
removeAllDebug() {
assert(
!this._locked,
'The plugin system is locked and removal operations are not allowed.',
);
this._debugs.forEach((fn) => fn());
}
getPluginApis(pluginName) {
return this.plugins[pluginName].apis;
}
listenError(fn) {
assert(
!this._locked,
'The plugin system is locked and cannot listen for errors.',
);
const map = Object.create(null);
for (const key in this.lifecycle) {
map[key] = (e) => {
fn(Object.assign(e, { name: key }));
};
this.lifecycle[key].listenError(map[key]);
}
return () => {
assert(
!this._locked,
'The plugin system is locked and the listening error cannot be removed.',
);
for (const key in this.lifecycle) {
this.lifecycle[key].errors.delete(map[key]);
}
};
}
useRefine(plugin) {
return this.use(plugin, INTERNAL);
}
use(plugin, _flag) {
assert(
!this._locked,
`The plugin system is locked and new plugins cannot be added${
plugin.name ? `(${plugin.name})` : ''
}.`,
);
if (typeof plugin === 'function') plugin = plugin(this);
assert(isPlainObject(plugin), 'Invalid plugin configuration.');
if (_flag === INTERNAL) {
plugin = {
version: plugin.version,
name: plugin.name || uuid(),
hooks: omit(plugin, ['name', 'version']),
};
}
const { name } = plugin;
assert(name && typeof name === 'string', 'Plugin must provide a "name".');
assert(!this.isUsed(name), `Repeat to register plugin hooks "${name}".`);
const register = (obj, once) => {
if (obj) {
for (const key in obj) {
assert(
hasOwn(this.lifecycle, key),
`"${key}" hook is not defined in plugin "${name}".`,
);
const tag = name.startsWith(PERFORMANCE_PLUGIN_PREFIX) ? '' : name;
if (once) {
this.lifecycle[key].once(tag, obj[key]);
} else {
this.lifecycle[key].on(tag, obj[key]);
}
}
}
};
register(plugin.hooks, false);
register(plugin.onceHooks, true);
this.plugins[name] = plugin;
return plugin;
}
remove(pluginName) {
assert(
!this._locked,
'The plugin system has been locked and the plugin cannot be cleared.',
);
assert(pluginName, 'Must provide a "name".');
if (hasOwn(this.plugins, pluginName)) {
const plugin = this.plugins[pluginName];
const rm = (obj) => {
if (obj) {
for (const key in obj) {
this.lifecycle[key].remove(obj[key]);
}
}
};
rm(plugin.hooks);
rm(plugin.onceHooks);
delete this.plugins[pluginName];
}
}
pickLifyCycle(keys) {
return pick(this.lifecycle, keys);
}
isUsed(pluginName) {
assert(pluginName, 'Must provide a "name".');
return hasOwn(this.plugins, pluginName);
}
create(callback) {
return new PluginSystem(callback(HOOKS));
}
clone(usePlugin) {
const newLifecycle = Object.create(null);
for (const key in this.lifecycle) {
newLifecycle[key] = this.lifecycle[key].clone();
}
const cloned = new this.constructor(newLifecycle);
if (usePlugin) {
for (const key in this.plugins) {
cloned.use(this.plugins[key]);
}
}
return cloned;
}
}
function createDanmakuLifeCycle() {
return new PluginSystem({
hide: new SyncHook(),
show: new SyncHook(),
pause: new SyncHook(),
resume: new SyncHook(),
beforeMove: new SyncHook(),
moved: new SyncHook(),
createNode: new SyncHook(),
appendNode: new SyncHook(),
removeNode: new SyncHook(),
beforeDestroy: new AsyncHook(),
destroyed: new SyncHook(),
});
}
function createManagerLifeCycle() {
const { lifecycle } = createDanmakuLifeCycle();
return new PluginSystem({
// Danmaku hooks
$show: lifecycle.show,
$hide: lifecycle.hide,
$pause: lifecycle.pause,
$resume: lifecycle.resume,
$beforeMove: lifecycle.beforeMove,
$moved: lifecycle.moved,
$createNode: lifecycle.createNode,
$appendNode: lifecycle.appendNode,
$removeNode: lifecycle.removeNode,
$beforeDestroy: lifecycle.beforeDestroy,
$destroyed: lifecycle.destroyed,
// Global hooks
format: new SyncHook(),
start: new SyncHook(),
stop: new SyncHook(),
show: new SyncHook(),
hide: new SyncHook(),
freeze: new SyncHook(),
unfreeze: new SyncHook(),
finished: new SyncHook(),
clear: new SyncHook(),
mount: new SyncHook(),
unmount: new SyncHook(),
init: new SyncHook(),
limitWarning: new SyncHook(),
push: new SyncHook(),
render: new SyncHook(),
updateOptions: new SyncHook(),
willRender: new SyncWaterfallHook(),
});
}
const scope = '$';
const cache = [];
function createDanmakuPlugin(plSys) {
const plugin = {
name: `__danmaku_plugin_${ids.bridge++}__`,
};
if (cache.length) {
for (const [k, nk] of cache) {
plugin[nk] = (...args) => {
return plSys.lifecycle[k].emit(...args);
};
}
} else {
const keys = Object.keys(plSys.lifecycle);
for (const k of keys) {
if (k.startsWith(scope)) {
const nk = k.replace(scope, '');
cache.push([k, nk]);
plugin[nk] = (...args) => {
return plSys.lifecycle[k].emit(...args);
};
}
}
}
return plugin;
}
class FacileDanmaku {
_options;
data;
loops = 0;
isLoop = false;
paused = false;
moving = false;
isEnded = false;
isFixedDuration = false;
rate;
duration;
recorder;
nextFrame = nextFrame;
type = 'facile';
track = null;
node = null;
moveTimer = null;
position = { x: 0, y: 0 };
pluginSystem = createDanmakuLifeCycle();
_internalStatuses;
_initData;
constructor(_options) {
this._options = _options;
this.data = _options.data;
this.rate = _options.rate;
this.duration = _options.duration;
this._internalStatuses = _options.internalStatuses;
this._initData = {
duration: _options.duration,
width: _options.container.width,
};
this.recorder = {
pauseTime: 0,
startTime: 0,
prevPauseTime: 0,
};
}
/**
* @internal
*/
_delInTrack() {
this._options.delInTrack(this);
if (this.track) {
this.track._remove(this);
}
}
/**
* @internal
*/
_summaryWidth() {
return this._options.container.width + this.getWidth();
}
/**
* @internal
*/
_getMovePercent() {
const { pauseTime, startTime, prevPauseTime } = this.recorder;
const ct = this.paused ? prevPauseTime : now();
const movePercent = (ct - startTime - pauseTime) / this.actualDuration();
if (this._options.progress && this._options.progress > 0) {
return movePercent + this._options.progress;
}
return movePercent;
}
/**
* @internal
*/
_getMoveDistance() {
if (!this.moving) return 0;
return this._getMovePercent() * this._summaryWidth();
}
/**
* @internal
*/
_getSpeed() {
const cw = this._summaryWidth();
if (cw == null) return 0;
return cw / this.actualDuration();
}
/**
* @internal
*/
_createNode() {
if (this.node) return;
this.node = document.createElement('div');
this._setStartStatus();
this.node.__danmaku__ = this;
this.pluginSystem.lifecycle.createNode.emit(this, this.node);
}
/**
* @internal
*/
_appendNode(container) {
if (!this.node || this.node.parentNode === container) return;
container.appendChild(this.node);
this.pluginSystem.lifecycle.appendNode.emit(this, this.node);
}
/**
* @internal
*/
_removeNode(_flag) {
if (!this.node) return;
const parentNode = this.node.parentNode;
if (!parentNode) return;
parentNode.removeChild(this.node);
if (_flag !== INTERNAL_FLAG) {
this.pluginSystem.lifecycle.removeNode.emit(this, this.node);
}
}
/**
* @internal
*/
_setOff() {
return new Promise((resolve) => {
if (!this.node) {
this.moving = false;
this.isEnded = true;
resolve();
return;
}
for (const key in this._internalStatuses.styles) {
this.setStyle(key, this._internalStatuses.styles[key]);
}
const w = this.getWidth();
const cw = this._options.container.width + w;
const negative = this.direction === 'left' ? 1 : -1;
this._internalStatuses.viewStatus === 'hide'
? this.hide(INTERNAL_FLAG)
: this.show(INTERNAL_FLAG);
const actualDuration = this.actualDuration();
this.setStyle('transform', `translateX(${negative * cw}px)`);
this.setStyle('transition', `transform linear ${actualDuration}ms`);
if (this._options.progress && this._options.progress > 0) {
const remainingTime = this._options.progress * actualDuration;
this.setStyle('transitionDelay', `${-1 * remainingTime}ms`);
}
if (this.direction !== 'none') {
this.setStyle(this.direction, `-${w}px`);
}
this.moving = true;
this.recorder.startTime = now();
this.pluginSystem.lifecycle.beforeMove.emit(this);
whenTransitionEnds(this.node).then(() => {
this.loops++;
this.moving = false;
this.isEnded = true;
this.pluginSystem.lifecycle.moved.emit(this);
resolve();
});
});
}
/**
* @internal
*/
_setStartStatus() {
this._internalStatuses.viewStatus === 'hide'
? this.hide(INTERNAL_FLAG)
: this.show(INTERNAL_FLAG);
this.setStyle('zIndex', '0');
this.setStyle('opacity', '0');
this.setStyle('transform', '');
this.setStyle('transition', '');
this.setStyle('position', 'absolute');
this.setStyle('top', `${this.position.y}px`);
if (this.direction !== 'none') {
this.setStyle(this.direction, '0');
}
}
/**
* @internal
*/
_updatePosition(p) {
if (typeof p.x === 'number') {
this.position.x = p.x;
}
if (typeof p.y === 'number') {
this.position.y = p.y;
this.setStyle('top', `${p.y}px`);
}
}
/**
* @internal
*/
_updateTrack(track) {
this.track = track;
if (track) {
track._add(this);
}
}
/**
* @internal
*/
_updateDuration(duration, updateInitData = true) {
this.isFixedDuration = true;
this.duration = duration;
if (updateInitData) {
this._initData.duration = duration;
}
}
/**
* @internal
*/
_format(oldWidth, oldHeight, newTrack) {
if (this.isEnded) {
this.destroy();
return;
}
// Don't let the rendering of danmaku exceed the container
if (
this._options.container.height !== oldHeight &&
this.getHeight() + newTrack.location.bottom >
this._options.container.height
) {
this.destroy();
return;
}
// As the x-axis varies, the motion area of danmu also changes
if (this._options.container.width !== oldWidth) {
const { width, duration } = this._initData;
const speed = (width + this.getWidth()) / duration;
this._updateDuration(this._summaryWidth() / s