UNPKG

danmu

Version:

Flexible, cross-platform, powerful danmu library.

1,810 lines (1,802 loc) 87.3 kB
/*! * 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