@silkytone/danmu
Version:
弹幕的简单实现,实现普通弹幕或高级弹幕。
350 lines (349 loc) • 9.8 kB
JavaScript
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
};