UNPKG

danmu

Version:

Flexible, cross-platform, powerful danmu library.

1,867 lines 52.3 kB
/*!
 * danmu.js
 * (c) 2024-2026 Imtaotao
 * Released under the MIT License.
 */
import {
  remove,
  raf,
  once,
  mathExprEvaluate,
  map,
  assert,
  now,
  Queue,
  batchProcess,
  hasOwn,
  loopSlice,
  isInBounds,
  isNumber,
  random,
  isEmptyObject,
} from 'aidly';
import {
  PluginSystem,
  SyncHook,
  SyncWaterfallHook,
  AsyncHook,
} from 'hooks-plugin';

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 getTrackIdx = (founds, rows, distribution = 'random') => {
  if (distribution === 'order') {
    for (let i = 0; i < rows; i++) {
      if (!founds.has(i)) {
        return i;
      }
    }
  }
  const n = getTrackRandomIdx(rows);
  return founds.has(n) ? getTrackIdx(founds, rows, distribution) : n;
};
const getTrackRandomIdx = (rows) => {
  const n = Math.floor(Math.random() * rows);
  return 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;
  }
}

function createDanmakuLifeCycle() {
  return new PluginSystem({
    hide: new SyncHook(),
    show: new SyncHook(),
    pause: new SyncHook(),
    resume: new SyncHook(),
    beforeMove: new SyncHook(),
    moved: new SyncHook(),
    reachEdge: 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,
    $reachEdge: lifecycle.reachEdge,
    $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) {
      // Use named function to make it identifiable as a bridge function
      plugin[nk] = function __danmaku_bridge_fn(...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]);
        // Use named function to make it identifiable as a bridge function
        plugin[nk] = function __danmaku_bridge_fn(...args) {
          return plSys.lifecycle[k].emit(...args);
        };
      }
    }
  }
  return plugin;
}
const hasRealListeners = (hook) => {
  if (hook.isEmpty()) return false;
  for (const listener of hook.listeners) {
    if (listener.name !== '__danmaku_bridge_fn') {
      return true;
    }
  }
  return false;
};
function hasAnyRealListeners(
  danmakuHook,
  managerPluginSystem,
  managerHookName,
) {
  const hasDanmakuUserListeners = hasRealListeners(danmakuHook);
  const hasManagerListeners =
    managerPluginSystem !== undefined &&
    managerPluginSystem.lifecycle[managerHookName] !== undefined &&
    !managerPluginSystem.lifecycle[managerHookName].isEmpty();
  return hasDanmakuUserListeners || hasManagerListeners;
}

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;
  _hasReachedEdge = false;
  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);
      if (this.direction !== 'none') {
        this._hasReachedEdge = false;
        this._monitorEdge();
      }
      whenTransitionEnds(this.node).then(() => {
        this.loops++;
        this.moving = false;
        this.isEnded = true;
        this.pluginSystem.lifecycle.moved.emit(this);
        resolve();
      });
    });
  }
  /**
   * @internal
   */
  _monitorEdge() {
    if (!this.node || !this.moving) return;
    // Check if there are any real user listeners interested in edge detection:
    // 1. Danmaku instance listeners: Could be from push({ plugin: { reachEdge() {} } })
    // 2. Manager listeners: Could be from create({ plugin: { $reachEdge() {} } })
    if (
      !hasAnyRealListeners(
        this.pluginSystem.lifecycle.reachEdge,
        this._options.managerPluginSystem,
        '$reachEdge',
      )
    ) {
      return;
    }
    const check = () => {
      if (this._hasReachedEdge || !this.moving || this.paused || !this.node) {
        return;
      }
      const containerRect =
        this._options.container.node?.getBoundingClientRect();
      if (!containerRect) return;
      const rect = this.node.getBoundingClientRect();
      // Edge detection logic:
      // 1. direction === 'left': danmaku moves from left to right, detect when right edge touches container's right edge
      //    - Initial position: danmaku starts outside container left side (left: -${width}px)
      //    - Moves right via translateX, triggers when rect.right approaches containerRect.right
      //    - Condition: rect.right >= containerRect.right - threshold
      //
      // 2. direction === 'right': danmaku moves from right to left, detect when left edge touches container's left edge
      //    - Initial position: danmaku starts outside container right side (right: -${width}px)
      //    - Moves left via translateX, triggers when rect.left approaches containerRect.left
      //    - Condition: rect.left <= containerRect.left + threshold
      //
      // threshold: Set to 1px to provide a tolerance range
      //    - Due to animation frame rate and floating point precision, danmaku position might skip exact edge values
      //    - Using threshold ensures the event triggers when danmaku is near the edge (±1px), preventing missed detections
      const threshold = 1;
      const hasReached =
        this.direction === 'left'
          ? rect.right >= containerRect.right - threshold
          : rect.left <= containerRect.left + threshold;
      if (hasReached) {
        this._hasReachedEdge = true;
        this.pluginSystem.lifecycle.reachEdge.emit(this);
        return;
      }
      requestAnimationFrame(check);
    };
    check();
  }
  /**
   * @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() / speed, false);
      if (!this.paused) {
        this.pause(INTERNAL_FLAG);
        this.resume(INTERNAL_FLAG);
      }
    }
  }
  /**
   * @internal
   */
  _reset() {
    this.loops = 0;
    this.paused = false;
    this.moving = false;
    this.position = { x: 0, y: 0 };
    this._removeNode();
    this._delInTrack();
    this._setStartStatus();
    this._updateTrack(null);
    this.setStyle('top', '');
    if (this.moveTimer) {
      this.moveTimer.clear();
      this.moveTimer = null;
    }
    this.recorder = {
      pauseTime: 0,
      startTime: 0,
      prevPauseTime: 0,
    };
    this._initData = {
      duration: this._options.duration,
      width: this._options.container.width,
    };
  }
  get direction() {
    return this._options.direction;
  }
  // When our total distance remains constant,
  // acceleration is inversely proportional to time.
  actualDuration() {
    return this.duration / this.rate;
  }
  setloop() {
    this.isLoop = true;
  }
  unloop() {
    this.isLoop = false;
  }
  getHeight() {
    return (this.node && this.node.clientHeight) || 0;
  }
  getWidth() {
    return (this.node && this.node.clientWidth) || 0;
  }
  pause(_flag) {
    if (!this.moving || this.paused) return;
    let d = this._getMoveDistance();
    if (Number.isNaN(d)) return;
    const negative = this.direction === 'left' ? 1 : -1;
    this.paused = true;
    this.recorder.prevPauseTime = now();
    this.setStyle('zIndex', '2');
    this.setStyle('transitionDuration', '0ms');
    this.setStyle('transform', `translateX(${d * negative}px)`);
    if (_flag !== INTERNAL_FLAG) {
      this.pluginSystem.lifecycle.pause.emit(this);
    }
  }
  resume(_flag) {
    if (!this.moving || !this.paused) return;
    const cw = this._summaryWidth();
    const negative = this.direction === 'left' ? 1 : -1;
    const remainingTime = (1 - this._getMovePercent()) * this.actualDuration();
    this.paused = false;
    this.recorder.pauseTime += now() - this.recorder.prevPauseTime;
    this.recorder.prevPauseTime = 0;
    this.setStyle('zIndex', '0');
    this.setStyle('transitionDuration', `${remainingTime}ms`);
    this.setStyle('transform', `translateX(${cw * negative}px)`);
    this.setStyle('transitionDelay', '');
    if (this.direction !== 'none' && !this._hasReachedEdge) {
      this._monitorEdge();
    }
    if (_flag !== INTERNAL_FLAG) {
      this.pluginSystem.lifecycle.resume.emit(this);
    }
  }
  hide(_flag) {
    this.setStyle('visibility', 'hidden');
    if (_flag !== INTERNAL_FLAG) {
      this.pluginSystem.lifecycle.hide.emit(this);
    }
  }
  show(_flag) {
    this.setStyle('visibility', 'visible');
    if (_flag !== INTERNAL_FLAG) {
      this.pluginSystem.lifecycle.show.emit(this);
    }
  }
  async destroy(mark) {
    await this.pluginSystem.lifecycle.beforeDestroy.emit(this, mark);
    this.moving = false;
    this._delInTrack();
    this._removeNode();
    if (this.moveTimer) {
      this.moveTimer.clear();
      this.moveTimer = null;
    }
    this.pluginSystem.lifecycle.destroyed.emit(this, mark);
    this.node = null;
  }
  setStyle(key, val) {
    if (!this.node) return;
    this.node.style[key] = val;
  }
  remove(pluginName) {
    this.pluginSystem.remove(pluginName);
  }
  use(plugin) {
    if (typeof plugin === 'function') plugin = plugin(this);
    if (!plugin.name) {
      plugin.name = `__facile_danmaku_plugin_${ids.danmu++}__`;
    }
    this.pluginSystem.useRefine(plugin);
    return plugin;
  }
}

class FlexibleDanmaku extends FacileDanmaku {
  _options;
  position;
  type = 'flexible';
  constructor(_options) {
    super(_options);
    this._options = _options;
    this.position = _options.position || { x: 0, y: 0 };
  }
  /**
   * @internal
   */
  _getSpeed() {
    if (this.direction === 'none') return 0;
    const { duration } = this._initData;
    const cw =
      this.direction === 'right'
        ? this.position.x + this.getWidth()
        : this.position.x;
    return cw / duration;
  }
  /**
   * @internal
   */
  _setOff() {
    return new Promise((resolve) => {
      if (!this.node) {
        this.moving = false;
        this.isEnded = true;
        resolve();
        return;
      }
      const onEnd = () => {
        this.loops++;
        this.moving = false;
        this.isEnded = true;
        if (this.moveTimer) {
          this.moveTimer.clear();
          this.moveTimer = null;
        }
        this.pluginSystem.lifecycle.moved.emit(this);
        resolve();
      };
      for (const key in this._internalStatuses.styles) {
        this.setStyle(key, this._internalStatuses.styles[key]);
      }
      this.moving = true;
      this.recorder.startTime = now();
      this.pluginSystem.lifecycle.beforeMove.emit(this);
      if (this.direction === 'none') {
        let timer = setTimeout(onEnd, this.actualDuration());
        this.moveTimer = {
          cb: onEnd,
          clear() {
            clearTimeout(timer);
            timer = null;
          },
        };
      } else {
        const ex =
          this.direction === 'left'
            ? this._options.container.width
            : -this.getWidth();
        this.setStyle(
          'transition',
          `transform linear ${this.actualDuration()}ms`,
        );
        this.setStyle(
          'transform',
          `translateX(${ex}px) translateY(${this.position.y}px)`,
        );
        this._hasReachedEdge = false;
        this._monitorEdge();
        whenTransitionEnds(this.node).then(onEnd);
      }
    });
  }
  /**
   * @internal
   */
  _setStartStatus() {
    this.setStyle('zIndex', '1');
    this.setStyle('transform', '');
    this.setStyle('transition', '');
    this.setStyle('position', 'absolute');
    this.setStyle(
      'transform',
      `translateX(${this.position.x}px) translateY(${this.position.y}px)`,
    );
    this._internalStatuses.viewStatus === 'hide'
      ? this.hide(INTERNAL_FLAG)
      : this.show(INTERNAL_FLAG);
  }
  /**
   * @internal
   */
  _updatePosition(p) {
    let needUpdateStyle = false;
    if (typeof p.x === 'number') {
      this.position.x = p.x;
      needUpdateStyle = true;
    }
    if (typeof p.y === 'number') {
      this.position.y = p.y;
      needUpdateStyle = true;
    }
    if (needUpdateStyle) {
      this.setStyle(
        'transform',
        `translateX(${this.position.x}px) translateY(${this.position.y}px)`,
      );
    }
  }
  /**
   * @internal
   */
  _getMovePercent(useInitData) {
    const { pauseTime, startTime, prevPauseTime } = this.recorder;
    const ct = this.paused ? prevPauseTime : now();
    const moveTime = ct - startTime - pauseTime;
    return (
      moveTime /
      (useInitData
        ? this._initData.duration / this.rate
        : this.actualDuration())
    );
  }
  /**
   * @internal
   */
  _getMoveDistance() {
    if (!this.moving) return 0;
    let d;
    let { x } = this.position;
    const diff = this._initData.width - this._options.container.width;
    if (this.direction === 'none') {
      d = x - diff;
    } else {
      const percent = this._getMovePercent(true);
      if (this.direction === 'left') {
        // When the container changes and the direction of movement is to the right,
        // there is no need for any changes
        d = x + (this._options.container.width - x) * percent;
      } else {
        d = x - (x + this.getWidth()) * percent - diff;
      }
    }
    return d;
  }
  /**
   * @internal
   */
  _format() {
    if (this.direction === 'left') return;
    if (this.direction === 'none') {
      this.setStyle(
        'transform',
        `translateX(${this._getMoveDistance()}px) translateY(${this.position.y}px)`,
      );
      return;
    }
    const diff = this._initData.width - this._options.container.width;
    const cw = this.position.x + this.getWidth();
    this._updateDuration((cw - diff) / this._getSpeed(), false);
    if (this.paused) {
      this.resume(INTERNAL_FLAG);
      this.pause(INTERNAL_FLAG);
    } else {
      this.pause(INTERNAL_FLAG);
      this.resume(INTERNAL_FLAG);
    }
  }
  pause(_flag) {
    if (!this.moving || this.paused) return;
    this.paused = true;
    this.recorder.prevPauseTime = now();
    if (this.direction === 'none') {
      if (this.moveTimer) this.moveTimer.clear();
    } else {
      this.setStyle('zIndex', '3');
      this.setStyle('transitionDuration', '0ms');
      this.setStyle(
        'transform',
        `translateX(${this._getMoveDistance()}px) translateY(${this.position.y}px)`,
      );
    }
    if (_flag !== INTERNAL_FLAG) {
      this.pluginSystem.lifecycle.pause.emit(this);
    }
  }
  resume(_flag) {
    if (!this.moving || !this.paused) return;
    this.paused = false;
    this.recorder.pauseTime += now() - this.recorder.prevPauseTime;
    this.recorder.prevPauseTime = 0;
    const remainingTime = (1 - this._getMovePercent()) * this.actualDuration();
    if (this.direction === 'none') {
      if (this.moveTimer) {
        let timer = setTimeout(this.moveTimer.cb || (() => {}), remainingTime);
        this.moveTimer.clear = () => {
          clearTimeout(timer);
          timer = null;
        };
      }
    } else {
      const ex =
        this.direction === 'left'
          ? this._options.container.width
          : -this.getWidth();
      this.setStyle('zIndex', '1');
      this.setStyle('transitionDuration', `${remainingTime}ms`);
      this.setStyle(
        'transform',
        `translateX(${ex}px) translateY(${this.position.y}px)`,
      );
    }
    if (_flag !== INTERNAL_FLAG) {
      this.pluginSystem.lifecycle.resume.emit(this);
    }
  }
  remove(pluginName) {
    this.pluginSystem.remove(pluginName);
  }
  use(plugin) {
    if (typeof plugin === 'function') plugin = plugin(this);
    if (!plugin.name) {
      plugin.name = `__flexible_danmaku_plugin_${ids.danmu++}__`;
    }
    this.pluginSystem.useRefine(plugin);
    return plugin;
  }
}

class Engine {
  _options;
  rows = 0;
  container = new Container();
  tracks = [];
  _fx = new Queue();
  _sets = {
    view: new Set(),
    flexible: new Set(),
    stash: [],
  };
  _retryMap = new WeakMap();
  // Avoid frequent deletion of danmaku.
  // collect the danmaku that need to be deleted within 2 seconds and delete them together.
  _addDestroyQueue = batchProcess({
    ms: 3000,
    processor: (ls) => ls.forEach((dm) => dm.destroy()),
  });
  _managerPluginSystem;
  constructor(_options) {
    this._options = _options;
  }
  /**
   * @internal
   */
  _setManagerPluginSystem(managerPluginSystem) {
    this._managerPluginSystem = managerPluginSystem;
  }
  len() {
    const { stash, view, flexible } = this._sets;
    return {
      stash: stash.length,
      flexible: flexible.size,
      view: view.size + flexible.size,
      all: view.size + flexible.size + stash.length,
    };
  }
  add(data, options, isUnshift) {
    const val = data instanceof FacileDanmaku ? data : { data, options };
    this._sets.stash[isUnshift ? 'unshift' : 'push'](val);
  }
  updateOptions(newOptions) {
    this._options = Object.assign(this._options, newOptions);
    if (hasOwn(newOptions, 'gap')) {
      this._options.gap = this.container._toNumber('width', this._options.gap);
    }
    if (hasOwn(newOptions, 'overlap')) {
      let overlap = this._options.overlap;
      if (overlap < 0) overlap = 0;
      this._options.overlap = overlap;
    }
    if (hasOwn(newOptions, 'trackHeight')) {
      this.format();
    }
  }
  clear(type) {
    if (!type || type === 'facile') {
      for (let i = 0; i < this.tracks.length; i++) {
        this.tracks[i].clear();
      }
      this._sets.view.clear();
      this._sets.stash.length = 0;
    }
    if (!type || type === 'flexible') {
      for (const dm of this._sets.flexible) {
        dm.destroy();
      }
      this._sets.flexible.clear();
    }
  }
  // `flexible` and `view` are both xx,
  // so deleting some of them in the loop will not affect
  each(fn) {
    for (const dm of this._sets.flexible) {
      if (!dm.isEnded) {
        if (fn(dm) === false) return;
      }
    }
    for (const dm of this._sets.view) {
      if (!dm.isEnded) {
        if (fn(dm) === false) return;
      }
    }
  }
  // Because there are copies brought by `Array.from`,
  // deleting it in all loops will not affect
  asyncEach(fn) {
    let stop = false;
    const arr = Array.from(this._sets.flexible);
    return loopSlice(
      arr.length,
      (i) => {
        if (!arr[i].isEnded) {
          if (fn(arr[i]) === false) {
            stop = true;
            return false;
          }
        }
      },
      17,
    ).then(() => {
      if (stop) return;
      const arr = Array.from(this._sets.view);
      return loopSlice(
        arr.length,
        (i) => {
          if (!arr[i].isEnded) {
            return fn(arr[i]);
          }
        },
        17,
      );
    });
  }
  format() {
    const { width, height } = this.container;
    this.container._format();
    const { gap, trackHeight } = this._options;
    this._options.gap = this.container._toNumber('width', gap);
    const h = this.container._toNumber('height', trackHeight);
    if (h <= 0) {
      for (let i = 0; i < this.tracks.length; i++) {
        this.tracks[i].clear();
      }
      return;
    }
    const rows = (this.rows = +(this.container.height / h).toFixed(0));
    for (let i = 0; i < rows; i++) {
      const track = this.tracks[i];
      const top = h * i;
      const bottom = h * (i + 1) - 1;
      const middle = (bottom - top) / 2 + top;
      const location = { top, middle, bottom };
      if (bottom > this.container.height) {
        this.rows--;
        if (track) {
          this.tracks[i].clear();
          this.tracks.splice(i, 1);
        }
      } else if (track) {
        // If the reused track is larger than the container height,
        // the overflow needs to be deleted.
        if (track.location.middle > this.container.height) {
          this.tracks[i].clear();
        } else {
          track.each((dm) => {
            dm._format(width, height, track);
          });
        }
        track._updateLocation(location);
      } else {
        const track = new Track({
          index: i,
          list: [],
          location,
          container: this.container,
        });
        this.tracks.push(track);
      }
    }
    // Delete the extra tracks and the danmaku inside
    if (this.tracks.length > this.rows) {
      for (let i = this.rows; i < this.tracks.length; i++) {
        this.tracks[i].clear();
      }
      this.tracks.splice(this.rows, this.tracks.length - this.rows);
    }
    // If `flexible` danmaku is also outside the view, it also needs to be deleted
    for (const dm of this._sets.flexible) {
      if (dm.position.y > this.container.height) {
        dm.destroy();
      } else if (width !== this.container.width) {
        dm._format();
      }
    }
  }
  renderFlexibleDanmaku(data, options, { hooks, statuses, danmakuPlugin }) {
    assert(this.container, 'Container not formatted');
    hooks.render.call(null, 'flexible');
    const dm = this._create('flexible', data, options, statuses);
    if (dm.position.x > this.container.width) return false;
    if (dm.position.y > this.container.height) return false;
    if (options.plugin) dm.use(options.plugin);
    dm.use(danmakuPlugin);
    const { prevent } = hooks.willRender.call(null, {
      type: 'flexible',
      danmaku: dm,
      prevent: false,
      trackIndex: null,
    });
    if (this._options.rate > 0 && prevent !== true) {
      const setup = () => {
        dm._createNode();
        this._sets.flexible.add(dm);
        this._setAction(dm, statuses).then((isFreeze) => {
          if (isFreeze) {
            // console.error(
            //   'Currently in a freeze state, unable to render "FlexibleDanmaku"',
            // );
            return;
          }
          if (dm.isLoop) {
            dm._setStartStatus();
            setup();
            return;
          }
          dm.destroy();
          if (this.len().all === 0) {
            hooks.finished.call(null);
          }
        });
      };
      setup();
      return true;
    }
    return false;
  }
  renderFacileDanmaku({ hooks, statuses, danmakuPlugin }) {
    const { mode, limits } = this._options;
    const launch = () => {
      const num = this.len();
      let l = num.stash;
      if (typeof limits.view === 'number') {
        const max = limits.view - num.view;
        if (l > max) l = max;
      }
      if (mode === 'strict' && l > this.rows) {
        l = this.rows;
      }
      if (l <= 0) return;
      hooks.render.call(null, 'facile');
      return loopSlice(l, () =>
        this._consumeFacileDanmaku(statuses, danmakuPlugin, hooks),
      );
    };
    if (mode === 'strict') {
      this._fx.add((next) => {
        const p = launch();
        p ? p.then(next) : next();
      });
    } else {
      launch();
    }
  }
  _consumeFacileDanmaku(statuses, danmakuPlugin, hooks) {
    let dm;
    const layer = this._sets.stash.shift();
    if (!layer) return;
    const track = this._getTrack();
    if (!track) {
      this._sets.stash.unshift(layer);
      // If there is nothing to render, return `false` to stop the loop.
      return false;
    }
    if (layer instanceof FacileDanmaku) {
      dm = layer;
    } else {
      dm = this._create('facile', layer.data, layer.options, statuses);
      if (layer.options.plugin) {
        dm.use(layer.options.plugin);
      }
      dm.use(danmakuPlugin);
    }
    const { prevent } = hooks.willRender.call(null, {
      type: 'facile',
      danmaku: dm,
      prevent: false,
      trackIndex: track.index,
    });
    // When the rate is less than or equal to 0,
    // the danmaku will never move, but it will be rendered,
    // so just don't render it here.
    if (this._options.rate > 0 && prevent !== true) {
      // First createNode, users may add styles
      dm._createNode();
      dm._appendNode(this.container.node);
      dm._updateTrack(track);
      const setup = () => {
        this._sets.view.add(dm);
        this._setAction(dm, statuses).then((isStash) => {
          if (isStash) {
            dm._reset();
            this._sets.view.delete(dm);
            const retries = (this._retryMap.get(dm) || 0) + 1;
            this._retryMap.set(dm, retries);
            if (retries > 1 && this.len().stash > 100) {
              // discard the danmaku when we have a lot of stash danmaku
              this._retryMap.delete(dm);
              this._addDestroyQueue(dm);
            } else {
              this._sets.stash.unshift(dm);
            }
            return;
          }
          if (dm.isLoop) {
            dm._setStartStatus();
            setup();
            return;
          }
          this._addDestroyQueue(dm);
          if (this.len().all === 0) {
            hooks.finished.call(null);
          }
        });
      };
      // Waiting for the style to take effect,
      // we need to get the danmaku screen height.
      let i = 0;
      const triggerSetup = () => {
        nextFrame(() => {
          const height = dm.getHeight();
          if (height === 0 && ++i < 20) {
            triggerSetup();
          } else {
            const y = track.location.middle - height / 2;
            if (y + height > this.container.height) return;
            dm._updatePosition({ y });
            setup();
          }
        });
      };
      triggerSetup();
    }
  }
  _setAction(cur, internalStatuses) {
    return new Promise((resolve) => {
      nextFrame(() => {
        if (internalStatuses.freeze === true) {
          resolve(true);
          return;
        }
        const { mode, durationRange } = this._options;
        if (cur.type === 'facile') {
          const speed = this._calculateSpeed(cur._options.speed);
          if (speed) {
            cur._updateDuration(cur._summaryWidth() / speed, true);
          } else if (mode === 'strict' || mode === 'adaptive') {
            assert(cur.track, 'Track not found');
            const prev = cur.track._last(1);
            if (prev && cur.loops === 0) {
              const fixTime = this._collisionPrediction(prev, cur);
              if (fixTime !== null) {
                if (isInBounds(durationRange, fixTime)) {
                  cur._updateDuration(fixTime, true);
                } else if (mode === 'strict') {
                  resolve(true);
                  return;
                }
              }
            }
          }
        } else if (cur.type === 'flexible') {
          cur.use({
            appendNode: () => {
              const speed = this._calculateSpeed(cur._options.speed);
              if (speed) {
                cur._updateDuration(
                  (cur.position.x + cur.getWidth()) / speed,
                  true,
                );
              }
            },
          });
        }
        cur._appendNode(this.container.node);
        nextFrame(() => {
          if (internalStatuses.freeze === true) {
            cur._removeNode(INTERNAL_FLAG);
            resolve(true);
          } else {
            cur._setOff().then(() => resolve(false));
          }
        });
      });
    });
  }
  _create(type, data, options, internalStatuses) {
    assert(this.container, 'Container not formatted');
    const config = {
      data,
      internalStatuses,
      rate: options.rate,
      speed: options.speed,
      container: this.container,
      duration: options.duration,
      direction: options.direction,
      progress: options.progress,
      managerPluginSystem: this._managerPluginSystem,
      delInTrack: (b) => {
        remove(this._sets.view, b);
        type === 'facile'
          ? remove(this._sets.stash, b)
          : remove(this._sets.flexible, b);
      },
    };
    if (type === 'facile') {
      // Create FacileDanmaku
      return new FacileDanmaku(config);
    } else {
      // Create FlexibleDanmaku
      const dm = new FlexibleDanmaku(config);
      const { position } = options;
      // If it is a function, the position will be updated after the node is created,
      // so that the function can get accurate danmaku data.
      if (typeof position === 'function') {
        dm.use({
          appendNode: () => {
            let { x, y } = position(dm, this.container);
            x = this.container._toNumber('width', x);
            y = this.container._toNumber('height', y);
            dm._updatePosition({ x, y });
          },
        });
      } else {
        const x = this.container._toNumber('width', position.x);
        const y = this.container._toNumber('height', position.y);
        dm._updatePosition({ x, y });
      }
      return dm;
    }
  }
  _calculateSpeed(s) {
    if (s && typeof s === 'string') {
      s = this.container._toNumber('width', s);
      assert(
        isNumber(s) && s > 0,
        `The speed must > 0, but the current value is "${s}"`,
      );
    }
    return s;
  }
  _getTrackRandom() {
    const i = getTrackRandomIdx(this.rows);
    const track = this.tracks[i];
    return track ?? null;
  }
  _getTrack(founds = new Set()) {
    if (this.rows === 0) return null;
    const { gap, mode, distribution } = this._options;
    if (founds.size === this.tracks.length) {
      return mode === 'adaptive' ? (this._getTrackRandom() ?? null) : null;
    }
    const i = getTrackIdx(founds, this.rows, distribution);
    const track = this.tracks[i];
    if (!track.isLock) {
      if (mode === 'none') {
        return track;
      }
      const last = track._last(0);
      if (!last) {
        return track;
      }
      const lastWidth = last.getWidth();
      if (lastWidth > 0 && last._getMoveDistance() >= gap + lastWidth) {
        return track;
      }
    }
    founds.add(i);
    return this._getTrack(founds);
  }
  _collisionPrediction(prv, cur) {
    const cs = cur._getSpeed();
    const ps = prv._getSpeed();
    const acceleration = cs - ps;
    if (acceleration <= 0) return null;
    const overlap = this._options.overlap;
    const cw = cur.getWidth() * (1 - overlap);
    const pw = prv.getWidth();
    const { gap } = this._options;
    const distance = prv._getMoveDistance() - cw - pw - gap;
    const collisionTime = distance / acceleration;
    if (collisionTime >= cur.duration) return null;
    assert(this.container, 'Container not formatted');
    const remainingTime = (1 - prv._getMovePercent()) * prv.duration;
    const currentFixTime = ((cw + gap) * remainingTime) / this.container.width;
    return remainingTime + currentFixTime;
  }
}

class Manager {
  options;
  version = '0.20.0';
  nextFrame = nextFrame;
  statuses = Object.create(null);
  pluginSystem = createManagerLifeCycle();
  _engine;
  _renderTimer = null;
  _internalStatuses = Object.create(null);
  constructor(options) {
    this.options = options;
    this._engine = new Engine(options);
    this._engine._setManagerPluginSystem(this.pluginSystem);
    this._internalStatuses.freeze = false;
    this._internalStatuses.viewStatus = 'show';
    this._internalStatuses.styles = Object.create(null);
    this._internalStatuses.styles.opacity = '';
    this.pluginSystem.lifecycle.init.emit(this);
    this.options.overlap ??= 0;
  }
  /**
   * @internal
   */
  _mergeOptions(pushOptions) {
    const options = pushOptions ? pushOptions : Object.create(null);
    if (!('rate' in options)) {
      options.rate = this.options.rate;
    }
    if (!('speed' in options)) {
      options.speed = this.options.speed;
    }
    if (!('direction' in options)) {
      options.direction = this.options.direction;
    }
    if (!('duration' in options)) {
      const duration = random(...this.options.durationRange);
      assert(duration > 0, `Invalid duration "${duration}"`);
      options.duration = duration;
    }
    return options;
  }
  /**
   * @internal
   */
  _setViewStatus(status, filter) {
    return new Promise((resolve) => {
      if (this._internalStatuses.viewStatus === status) {
        resolve();
        return;
      }
      this._internalStatuses.viewStatus = status;
      this.pluginSystem.lifecycle[status].emit();
      this._engine
        .asyncEach((b) => {
          if (this._internalStatuses.viewStatus === status) {
            if (!filter || filter(b) !== true) {
              b[status]();
            }
          } else {
            return false;
          }
        })
        .then(resolve);
    });
  }
  get container() {
    return this._engine.container;
  }
  get trackCount() {
    return this._engine.tracks.length;
  }
  len() {
    return this._engine.len();
  }
  isShow() {
    return this._internalStatuses.viewStatus === 'show';
  }
  isFreeze() {
    return this._internalStatuses.freeze === true;
  }
  isPlaying() {
    return this._renderTimer !== null;
  }
  isDanmaku(dm) {
    return dm instanceof FacileDanmaku || dm instanceof FlexibleDanmaku;
  }
  each(fn) {
    this._engine.each(fn);
  }
  asyncEach(fn) {
    return this._engine.asyncEach(fn);
  }
  getTrack(i) {
    i = i >= 0 ? i : this.trackCount + i;
    return this._engine.tracks[i];
  }
  freeze({ preventEvents = [] } = {}) {
    let stopFlag;
    let pauseFlag;
    if (preventEvents.includes('stop')) {
      stopFlag = INTERNAL_FLAG;
    }
    if (preventEvents.includes('pause')) {
      pauseFlag = INTERNAL_FLAG;
    }
    this.stopPlaying(stopFlag);
    this.each((dm) => dm.pause(pauseFlag));
    this._internalStatuses.freeze = true;
    this.pluginSystem.lifecycle.freeze.emit();
  }
  unfreeze({ preventEvents = [] } = {}) {
    let startFlag;
    let resumeFlag;
    if (preventEvents.includes('start')) {
      startFlag = INTERNAL_FLAG;
    }
    if (preventEvents.includes('resume')) {
      resumeFlag = INTERNAL_FLAG;
    }
    this.each((dm) => dm.resume(resumeFlag));
    this.startPlaying(startFlag);
    this._internalStatuses.freeze = false;
    this.pluginSystem.lifecycle.unfreeze.emit();
  }
  format() {
    this._engine.format();
    this.pluginSystem.lifecycle.format.emit();
  }
  mount(parentNode, { clear = true } = {}) {
    if (parentNode) {
      if (typeof parentNode === 'string') {
        const res = document.querySelector(parentNode);
        assert(res, `Invalid selector "${parentNode}"`);
        parentNode = res;
      }
      if (this.isPlaying()) {
        clear && this.clear(null, INTERNAL_FLAG);
      }
      this._engine.container._mount(parentNode);
      this.format();
      this.pluginSystem.lifecycle.mount.emit(parentNode);
    }
  }
  unmount() {
    const { parentNode } = this.container;
    this.container._unmount();
    this.pluginSystem.lifecycle.unmount.emit(parentNode);
  }
  clear(type, _flag) {
    this._engine.clear(type);
    if (_flag !== INTERNAL_FLAG) {
      this.pluginSystem.lifecycle.clear.emit(type);
    }
  }
  updateOptions(newOptions, key) {
    this._engine.updateOptions(newOptions);
    newOptions.overlap ??= this.options.overlap;
    this.options = Object.assign(this.options, newOptions);
    if (hasOwn(newOptions, 'interval')) {
      this.stopPlaying(INTERNAL_FLAG);
      this.startPlaying(INTERNAL_FLAG);
    }
    this.pluginSystem.lifecycle.updateOptions.emit(newOptions, key);
  }
  startPlaying(_flag) {
    if (this.isPlaying()) return;
    if (_flag !== INTERNAL_FLAG) {
      this.pluginSystem.lifecycle.start.emit();
    }
    const cycle = () => {
      this._renderTimer = setTimeout(cycle, this.options.interval);
      this.render();
    };
    cycle();
  }
  stopPlaying(_flag) {
    if (!this.isPlaying()) return;
    if (this._renderTimer) {
      clearTimeout(this._renderTimer);
    }
    this._renderTimer = null;
    if (_flag !== INTERNAL_FLAG) {
      this.pluginSystem.lifecycle.stop.emit();
    }
  }
  show(filter) {
    return this._setViewStatus('show', filter);
  }
  hide(filter) {
    return this._setViewStatus('hide', filter);
  }
  canPush(type) {
    let res = true;
    const isFacile = type === 'facile';
    const { limits } = this.options;
    const { stash, view } = this._engine.len();
    if (isFacile) {
      res = stash < limits.stash;
    } else if (typeof limits.view === 'number') {
      res = view < limits.view;
    }
    return res;
  }
  unshift(data, options) {
    return this.push(data, options, INTERNAL_FLAG);
  }
  push(data, options, _unshift) {
    if (!this.canPush('facile')) {
      const { stash } = this.options.limits;
      const hook = this.pluginSystem.lifecycle.limitWarning;
      !hook.isEmpty()
        ? hook.emit('facile', stash)
        : console.warn(
            `The number of danmu in temporary storage exceeds the limit (${stash})`,
          );
      return false;
    }
    const isUnshift = _unshift === INTERNAL_FLAG;
    if (!this.isDanmaku(data)) {
      options = this._mergeOptions(options);
    }
    this._engine.add(data, options, isUnshift);
    this.pluginSystem.lifecycle.push.emit(data, 'facile', isUnshift);
    return true;
  }
  pushFlexibleDanmaku(data, options) {
    if (!this.isPlaying()) return false;
    if (typeof options.duration === 'number' && options.duration < 0) {
      return false;
    }
    if (!this.canPush('flexible')) {
      const { view } = this.options.limits;
      const hook = this.pluginSystem.lifecycle.limitWarning;
      !hook.isEmpty()
        ? hook.emit('flexible', view || 0)
        : console.warn(
            `The number of danmu in view exceeds the limit (${view})`,
          );
      return false;
    }
    const res = this._engine.renderFlexibleDanmaku(
      data,
      this._mergeOptions(options),
      {
        statuses: this._internalStatuses,
        danmakuPlugin: createDanmakuPlugin(this.pluginSystem),
        hooks: {
          finished: () => this.pluginSystem.lifecycle.finished.emit(),
          render: (val) => this.pluginSystem.lifecycle.render.emit(val),
          willRender: (val) => this.pluginSystem.lifecycle.willRender.emit(val),
        },
      },
    );
    if (res) {
      this.pluginSystem.lifecycle.push.emit(data, 'flexible', false);
      return true;
    }
    return false;
  }
  updateOccludedUrl(url, el) {
    let set;
    if (el) {
      if (typeof el === 'string') {
        const res = document.querySelector(el);
        assert(res, `Invalid selector "${el}"`);
        el = res;
      }
      set = (key, val) => (el.style[key] = val);
    } else {
      set = (key, val) => this.container.setStyle(key, val);
    }
    if (url) {
      assert(typeof url === 'string', `Invalid url "${url}"`);
      set('maskImage', `url("${url}")`);
      set('webkitMaskImage', `url("${url}")`);
      set('maskSize', 'cover');
      set('webkitMaskSize', 'cover');
    } else {
      set('maskImage', 'none');
      set('webkitMaskImage', 'none');
    }
  }
  render() {
    this._engine.renderFacileDanmaku({
      statuses: this._internalStatuses,
      danmakuPlugin: createDanmakuPlugin(this.pluginSystem),
      hooks: {
        finished: () => this.pluginSystem.lifecycle.finished.emit(),
        render: (val) => this.pluginSystem.lifecycle.render.emit(val),
        willRender: (val) => this.pluginSystem.lifecycle.willRender.emit(val),
      },
    });
  }
  remove(pluginName) {
    this.pluginSystem.remove(pluginName);
  }
  use(plugin) {
    if (typeof plugin === 'function') plugin = plugin(this);
    if (!plugin.name) {
      plugin.name = `__runtime_plugin_${ids.runtime++}__`;
    }
    this.pluginSystem.useRefine(plugin);
    return plugin;
  }
  setStyle(key, val) {
    const { styles } = this._internalStatuses;
    if (styles[key] !== val) {
      styles[key] = val;
      this._engine.asyncEach((dm) => {
        if (dm.moving) {
          dm.setStyle(key, val);
        }
      });
    }
  }
  setOpacity(opacity) {
    if (typeof opacity === 'string') {
      opacity = Number(opacity);
    }
    if (opacity < 0) {
      opacity = 0;
    } else if (opacity > 1) {
      opacity = 1;
    }
    this.setStyle('opacity', String(opacity));
  }
  setArea(size) {
    if (!isEmptyObject(size)) {
      this._engine.container._updateSize(size);
      this.format();
    }
  }
  setGap(gap) {
    this.updateOptions({ gap }, 'gap');
  }
  setMode(mode) {
    this.updateOptions({ mode }, 'mode');
  }
  setSpeed(speed) {
    this.updateOptions({ speed }, 'speed');
  }
  setRate(rate) {
    if (rate !== this.options.rate) {
      this.updateOptions({ rate }, 'rate');
    }
  }
  setInterval(interval) {
    this.updateOptions({ interval }, 'interval');
  }
  setTrackHeight(trackHeight) {
    this.updateOptions({ trackHeight }, 'trackHeight');
  }
  setDurationRange(durationRange) {
    this.updateOptions({ durationRange }, 'durationRange');
  }
  setDirection(direction) {
    this.updateOptions({ direction }, 'direction');
  }
  setOverlap(overlap) {
    if (overlap < 0) overlap = 0;
    this.updateOptions({ overlap }, 'overlap');
  }
  setLimits({ view, stash }) {
    let needUpdate = false;
    const limits = Object.assign({}, this.options.limits);
    if (typeof view === 'number') {
      needUpdate = true;
      limits.view = view;
    }
    if (typeof stash === 'number') {
      needUpdate = true;
      limits.stash = stash;
    }
    if (needUpdate) {
      this.updateOptions({ limits }, 'limits');
    }
  }
}

const formatOptions = (options) => {
  const newOptions = Object.assign(
    {
      gap: 0,
      rate: 1,
      limits: {},
      interval: 500,
      mode: 'strict',
      direction: 'right',
      trackHeight: '20%',
      durationRange: [4000, 6000],
    },
    options,
  );
  assert(newOptions.gap >= 0, 'Gap must be greater than or equal to 0');
  if (typeof newOptions.limits.stash !== 'number') {
    newOptions.limits.stash = Infinity;
  }
  return newOptions;
};
function create(options) {
  const opts = formatOptions(options);
  const manager = new Manager(opts);
  if (opts.plugin) {
    const plugins = Array.isArray(opts.plugin) ? opts.plugin : [opts.plugin];
    for (const plugin of plugins) {
      manager.use(plugin);
    }
    manager.pluginSystem.lifecycle.init.emit(manager);
  }
  return manager;
}

export { create };