UNPKG

@silkytone/danmu

Version:

弹幕的简单实现,实现普通弹幕或高级弹幕。

350 lines (349 loc) 9.8 kB
function v(i) { return Object.prototype.toString.call(i).slice(8, -1); } function c(i, t) { return v(i).toLowerCase() === t.toLowerCase(); } function g(i) { return c(i, "array"); } function m(i) { return x(i) ? !1 : c(i, "object"); } function x(i) { return c(i, "function"); } function w(i) { return c(i, "string"); } function O(i) { return c(i, "number"); } function A(i) { return c(i, "boolean"); } function S(i) { return ["", null, void 0].includes(i) ? !0 : (g(i) ? i : Object.keys(i)).length === 0; } function T(i, ...t) { return t.reduce((e, s) => { if (S(s)) return e; for (const [n, r] of Object.entries(s)) { const o = e[n]; m(r) && m(o) ? e[n] = T(o, r) : e[n] = r; } return e; }, { ...i }); } function _(i) { return w(i) ? i : Object.keys(i).reduce((t, e) => (t.push(`${e}:${i[e]};`), t), []).join(""); } function E(i) { return g(i) ? i.filter(Boolean).join(" ") : m(i) && !w(i) ? Object.keys(i).reduce((t, e) => (i[e] && t.push(e), t), []).join(" ") : i; } function P(i, t) { for (const [e, s] of Object.entries(t)) i.addEventListener(e, s); } function k(i, t = {}, e = []) { const s = document.createElement(i); for (const [n, r] of Object.entries(t)) n === "style" ? s.style.cssText = _(r) : n === "class" ? s.className = E(r) : n === "on" ? P(s, r) : s.setAttribute(n, r); if (e.length) { const n = e.map((r) => w(r) ? document.createTextNode(r) : r); s.replaceChildren(...n); } return s; } const h = () => { var i; return ((i = window.performance) == null ? void 0 : i.now()) || Date.now(); }, R = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, VNode: k, getTime: h, getType: v, isArray: g, isBoolean: A, isEmpty: S, isFunction: x, isNumber: O, isObject: m, isString: w, isType: c, mergeObject: T }, Symbol.toStringTag, { value: "Module" })); class b { // TODO: 事件 listener = {}; // TODO: 监听 on(t, e) { this.listener[t] || (this.listener[t] = []), this.listener[t].push(e); } // TODO: 取消监听 off(t, e) { this.listener[t] && (this.listener[t] = this.listener[t].filter((s) => s !== e)); } // TODO: 触发 emit(t, e) { this.listener[t] && this.listener[t].forEach((s) => { s(e); }); } // TODO: 监听一次 once(t, e) { const s = (n) => { this.off(t, s), e(n); }; this.on(t, s); } // TODO: 清空 clear() { this.listener = {}; } } const p = function(i) { return window.requestAnimationFrame || i.webkitRequestAnimationFrame || i.mozRequestAnimationFrame || i.oRequestAnimationFrame || i.msRequestAnimationFrame || /* @__PURE__ */ function() { let t = 0; return function(e) { i.setTimeout(() => { e(h() - t); }, 1e3 / 6); }; }(); }(window); class F { // TODO: count = 0; // TODO: 限制帧数 __limit = 0; // TODO: 状态 status = !1; // TODO: 上一次的时间 last = { fps: 0, records: 0 }; // TODO: 监听器 listener = new b(); // TODO: 限制帧数 get limit() { return this.__limit; } // TODO: 限制帧数 set limit(t) { if (isNaN(t)) throw new Error("FPS must be a number"); if ([1 / 0, -1 / 0].includes(t)) throw new Error("FPS cannot be Infinity, set it to 0 if you need to remove the restriction"); this.__limit = t; } constructor(t = 0) { this.limit = t, this.status = !1; } getFps() { const t = h(); this.last.fps ? (this.count += 1, t - this.last.fps > 1e3 && (this.listener.emit("FPS", this.count), this.last.fps = t, this.count = 1)) : (this.count = 1, this.last.fps = t); } // TODO: 实时帧数 getRecords() { const t = h(); if (this.last && this.last.records !== t) { const e = 1e3 / (t - this.last.records); this.listener.emit("RealTimeFPS", parseFloat(e.toFixed(1))); } this.last.records = t; } // TODO: 绑定事件 on(t, e) { if (!["FPS", "RealTimeFPS"].includes(t)) throw new Error("Invalid listener type"); this.listener.on(t, e); } // TODO: 解绑事件 off(t, e) { if (!["FPS", "RealTimeFPS"].includes(t)) throw new Error("Invalid listener type"); this.listener.off(t, e); } // TODO: 开始 run(t) { this.status = !0; let e = h(); const s = (n = 0) => { if (this.status) if (this.getFps(), this.getRecords(), this.limit) { p(s); const r = 1e3 / this.limit; setTimeout(() => t(n), r - (h() - e) % r); } else t(n), p(s); }; p(s); } // TODO: 暂停,阻止下一帧的运行 stop() { this.status = !1; } } class $ { gap; tracks; trackSize; size; occupied; constructor({ width: t, height: e, trackSize: s = 20, tracks: n = void 0, gap: r = 0 }) { this.size = { width: t, height: e }, this.gap = r || 0, n !== void 0 ? (this.tracks = n, this.trackSize = Math.round((e - (n - 1) * r) / n)) : (this.trackSize = s, this.tracks = Math.max(Math.floor((e + r) / (s + r)), 1)), this.occupied = Array.from({ length: this.tracks }).map(() => []); } // TODO: 轨道转像素 trackToPx(t) { return t * (this.trackSize + this.gap); } // TODO: 清理 prune(t) { t = t || Date.now(), this.occupied.forEach((e, s) => { this.occupied[s] = e.filter(({ end: n }) => n > t); }); } // TODO: 检查 check(t, e, s) { this.prune(e); const { tracks: n, trackSize: r, gap: o } = this, a = Math.ceil(t / (r + o)); if (!(n < a)) for (let l = 0, u = n - a; l < u; l += 1) { let f = 0; for (let d = 0; d < a; d += 1) { const y = this.occupied[l + d]; if (!y.length) f += 1; else { const { end: j, delay: z } = y[y.length - 1]; if (j + z < s) f += 1; else { l += d, f = 0; break; } } if (f >= a) return { index: l, count: a }; } } } // TODO: 添加 add(t, e, s, n) { const r = Math.ceil(t / (this.size.width / s)), o = n || Date.now(), a = o + s, l = this.check(e, o, a); if (!l) return null; for (let u = 0; u < l.count; u++) this.occupied[l.index + u].push({ start: o, end: a, delay: r }); return l; } // TODO: 销毁 destroy() { this.occupied = Array.from({ length: this.tracks }).map(() => []); } } const L = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, Animation: F, AnimationFrame: p, Listener: b, Tracks: $ }, Symbol.toStringTag, { value: "Module" })); class M { listener; get height() { return this.$el.offsetHeight; } get width() { return this.$el.offsetWidth; } $el; constructor(t) { t.style.transform = "translateX(0)", t.style.willChange = "transform", t.style.position = "absolute", t.style.left = "100%", t.style.top = "0", this.$el = t, this.listener = new b(); } destroy() { this.$el.remove(); } on(t, e) { this.listener.on(t, e); } off(t, e) { this.listener.off(t, e); } once(t, e) { this.listener.once(t, e); } } class B extends M { duration; constructor(t, e = 3) { super(k("div", { class: "var-barrage-item is-text", style: { display: "inline-block" } }, [ k("span", {}, [t]) ])), this.duration = e, this.$el.style.whiteSpace = "nowrap"; } } class C { tracks; $el; animation; opt; size; // TODO: 移动区 moveTracks = []; waitTracks = []; status = !1; constructor(t, e) { this.opt = T({ limit: 60 }, e || {}), this.$el = this.createElement(t), this.animation = new F(this.opt.limit), this.size = { width: this.$el.clientWidth, height: this.$el.clientHeight }, this.tracks = new $({ ...this.opt, ...this.size }); } createElement(t) { const e = (() => { if (typeof t == "string") { const s = document.querySelector(t); if (!s) throw new Error("element not found"); return s; } else return t; })(); return e.innerHTML = "", e.style.width = "100%", e.style.height = "100%", e.style.overflow = "hidden", e.style.userSelect = "none", e.style.position = "relative", e.style.pointerEvents = "none", e; } playAnimation() { if (this.status) return; this.status = !0; const t = this.size.width; this.animation.run(() => { const e = Date.now(); for (const [s, n] of Object.entries(this.moveTracks)) { const r = (e - n.start) * n.step, o = t + n.value.width; r > o ? (n.value.destroy(), this.moveTracks.splice(parseInt(s), 1)) : n.value.$el.style.transform = `translateX(-${r}px)`; } this.addWaitTracks(), this.moveTracks.length || (this.animation.stop(), this.status = !1); }); } addWaitTracks() { if (!this.waitTracks.length) return !1; this.waitTracks = this.waitTracks.filter((t) => { const e = Date.now(), { $el: s, width: n, height: r, duration: o } = t, a = this.tracks.add(n, r, o, e); if (a) { s.style.top = this.tracks.trackToPx(a.index) + "px"; const l = Math.ceil(this.size.width / o * 1e6) / 1e6; return this.moveTracks.push({ start: e, step: l, value: t }), this.playAnimation(), null; } return t; }); } push(t) { this.$el.appendChild(t.$el); const e = Date.now(), { $el: s, width: n, height: r, duration: o } = t, a = this.tracks.add(n, r, o, e); if (a) { s.style.top = this.tracks.trackToPx(a.index) + "px"; const l = Math.ceil(this.size.width / o * 1e6) / 1e6; this.moveTracks.push({ start: e, step: l, value: t }), this.playAnimation(); } else this.waitTracks.push(t); } } export { C as Barrage, L as Lib, B as TextBarrage, R as Utils };