var x = Object.defineProperty; var L = (n, t, e) => t in n ? x(n, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : n[t] = e; var r = (n, t, e) => (L(n, typeof t != "symbol" ? t + "" : t, e), e); function S() { let n = document.createElement("canvas"), t = n.getContext("2d"), e = function() { let i = window.devicePixelRatio || 1, s = t.webkitBackingStorePixelRatio || t.mozBackingStorePixelRatio || t.msBackingStorePixelRatio || t.oBackingStorePixelRatio || t.backingStorePixelRatio || 1; return i / s; }(); return n.width = n.height = 0, e; } const w = { getDevicePixelRatio: S }; function I(n) { let t = 0; return n.forEach((e) => t += e), t; } function O(n) { if (n.length === 0) return null; let t = n.reduce((s, a) => ({ ...s, [a]: (s[a] || 0) + 1 }), {}), e = 0, i = null; for (const s in t) t[s] > e && (e = t[s], i = parseInt(s)); return i; } function E(n, t) { return n = Math.ceil(n), t = Math.floor(t), Math.floor(Math.random() * (t - n + 1)) + n; } const k = { sum: I, findMode: O, getRandomInt: E }, p = {}; function b(n) { return p[n] || (p[n] = document.createElement("img"), p[n].src = n), p[n]; } const F = { imageElementFactory: b }; function D(n, t) { let e = 0, i = n.length - 1; for (; e <= i; ) { let s = Math.floor((e + i) / 2); n[s].time < t.time ? e = s + 1 : i = s - 1; } n.splice(e, 0, t); } const H = { insertBarrageByTime: D }, c = { Canvas: w, Math: k, Cache: F, Algorithm: H }; class v { constructor({ id: t, time: e, text: i, fontSize: s, lineHeight: a, color: o, prior: h = !1, customRender: l, addition: g }, d) { // 弹幕的唯一标识 r(this, "id"); // 弹幕的出现时间(毫秒为单位) r(this, "time"); // 弹幕的内容 r(this, "text"); // 弹幕的字体大小 r(this, "fontSize"); // 弹幕的行高 r(this, "lineHeight"); // 弹幕颜色 r(this, "color"); // 是不是重要的 r(this, "prior"); // 自定义 render 相关配置 r(this, "customRender"); // 额外,附加的信息 r(this, "addition"); // 渲染器实例 r(this, "br"); // 用于描述渲染时弹幕整体的 top 和 left r(this, "top"); r(this, "left"); // 用于描述弹幕整体的尺寸 r(this, "width"); r(this, "height"); // 根据 text 解析成的片段数组 r(this, "sections", []); this.id = t, this.time = e, this.text = i, this.fontSize = s, this.lineHeight = a, this.color = o, this.prior = h, this.customRender = l, this.addition = g, this.br = d, this.initBarrage(); } /** * 进行当前弹幕相关数据的计算 */ initBarrage() { var o, h; const t = this.analyseText(this.text); let e, i = 0, s = 0; const a = []; t.forEach((l) => { var g, d; if (l.type === "image" && (e = (g = this.br.barrageImages) == null ? void 0 : g.find((f) => `[${f.id}]` === l.value))) i += e.width, s = s < e.height ? e.height : s, a.push(new A({ ...e, leftOffset: c.Math.sum(a.map((f) => f.width)) })); else { this.setCtxFont(this.br.ctx); const f = ((d = this.br.ctx) == null ? void 0 : d.measureText(l.value).width) || 0, R = this.fontSize * this.lineHeight; i += f, s = s < R ? R : s, a.push(new M({ text: l.value, width: f, height: R, leftOffset: c.Math.sum(a.map((C) => C.width)) })); } }), this.sections = a, this.width = ((o = this.customRender) == null ? void 0 : o.width) ?? i, this.height = ((h = this.customRender) == null ? void 0 : h.height) ?? s, this.sections.forEach((l) => { l.sectionType === "text" ? l.topOffset = (this.height - this.fontSize) / 2 : l.topOffset = (this.height - l.height) / 2; }); } /** * 解析 text 内容 * 文本内容[图片id]文本内容[图片id] => ['文本内容', '[图片id]', '文本内容', '[图片id]'] * @param barrageText 弹幕文本 */ analyseText(t) { const e = []; for (; t; ) { const a = t.indexOf("]"); if (a !== -1) { const o = t.lastIndexOf("[", a); o !== -1 ? (o !== 0 && e.push({ type: "text", value: t.slice(0, o) }), e.push({ type: a - o > 1 ? "image" : "text", value: t.slice(o, a + 1) }), t = t.slice(a + 1)) : (e.push({ type: "text", value: t.slice(0, a + 1) }), t = t.slice(a + 1)); } else e.push({ type: "text", value: t }), t = ""; } const i = []; let s = ""; for (let a = 0; a < e.length; a++) e[a].type === "text" ? s += e[a].value : (s !== "" && (i.push({ type: "text", value: s }), s = ""), i.push(e[a])); return s !== "" && i.push({ type: "text", value: s }), i; } /** * 将当前弹幕渲染到指定的上下文 * @param ctx 渲染上下文 */ render(t) { if (this.customRender) { this.customRender.renderFn({ ctx: t, barrage: this, br: this.br, imageElementFactory: c.Cache.imageElementFactory }); return; } this.setCtxFont(t), t.fillStyle = this.color, this.sections.forEach((e) => { e.sectionType === "text" ? t.fillText(e.text, this.left + e.leftOffset, this.top + e.topOffset) : e.sectionType === "image" && t.drawImage( c.Cache.imageElementFactory(e.url), this.left + e.leftOffset, this.top + e.topOffset, e.width, e.height ); }), (this.br.devConfig.isRenderBarrageBorder || this.prior) && (t.strokeStyle = "#89D5FF", t.strokeRect(this.left, this.top, this.width, this.height)); } /** * 设置上下文的 font 属性 * @param ctx 渲染上下文 */ setCtxFont(t) { t.font = `${this.br.renderConfig.fontWeight} ${this.fontSize}px ${this.br.renderConfig.fontFamily}`; } } class M { constructor({ text: t, width: e, height: i, leftOffset: s }) { r(this, "sectionType", "text"); r(this, "text"); r(this, "width"); r(this, "height"); r(this, "topOffset"); r(this, "leftOffset"); this.text = t, this.width = e, this.height = i, this.leftOffset = s; } } class A { constructor({ id: t, url: e, width: i, height: s, leftOffset: a }) { r(this, "sectionType", "image"); r(this, "id"); r(this, "url"); r(this, "width"); r(this, "height"); r(this, "topOffset"); r(this, "leftOffset"); this.id = t, this.url = e, this.width = i, this.height = s, this.leftOffset = a; } } class B extends v { constructor(e, i) { super(e, i); // 弹幕类型 r(this, "barrageType"); // 弹幕持续时间 r(this, "duration"); // 弹幕结束时间 r(this, "endTime"); const { barrageType: s, duration: a } = e; this.barrageType = s, this.duration = a, this.endTime = this.time + a, this.calcFixedBarrageLeft(); } /** * 计算固定弹幕的 left 属性 */ calcFixedBarrageLeft() { this.left = (this.br.canvasSize.width - this.width) / 2; } } class T extends v { constructor(e, i) { super(e, i); r(this, "barrageType", "scroll"); // 用于描述滚动弹幕在播放进度为 0 时,滚动弹幕左侧距离 Canvas 左侧的距离 r(this, "originalLeft"); // 用于描述滚动弹幕在播放进度为 0 时,滚动弹幕右侧距离 Canvas 左侧的距离 r(this, "originalRight"); // 标识当前的滚动弹幕是否应该显示,当设置不允许遮挡的话,部分滚动弹幕会不显示 r(this, "show", !0); // 当前弹幕会占据几个实际轨道 r(this, "grade"); this.calcOriginal(); } /** * 计算原始的 left 和 right 位置 */ calcOriginal() { this.originalLeft = this.br.canvasSize.width + this.time / 1e3 * this.br.renderConfig.speed, this.originalRight = this.originalLeft + this.width; } } class y extends v { constructor(e, i) { super(e, i); r(this, "barrageType", "senior"); // 高级弹幕配置 r(this, "seniorBarrageConfig"); // 用于描述高级弹幕在 x、y 轴上的运动速度 r(this, "vx"); r(this, "vy"); // 实际的 起始点 和 结束点 r(this, "actualStartLocation"); r(this, "actualEndLocation"); this.seniorBarrageConfig = e.seniorBarrageConfig, this.calcActualLocation(); } /** * 计算关键点的实际坐标 */ calcActualLocation() { const { startLocation: e, endLocation: i, motionDuration: s } = this.seniorBarrageConfig; let a = (e.type || "PIXEL") === "PIXEL" ? e.x : e.x * this.canvasSize.width, o = (e.type || "PIXEL") === "PIXEL" ? e.y : e.y * this.canvasSize.height; e.offsetX && (a += e.offsetX), e.offsetY && (o += e.offsetY), this.actualStartLocation = { x: a, y: o }; let h = (i.type || "PIXEL") === "PIXEL" ? i.x : i.x * this.canvasSize.width, l = (i.type || "PIXEL") === "PIXEL" ? i.y : i.y * this.canvasSize.height; i.offsetX && (h += i.offsetX), i.offsetY && (l += i.offsetY), this.actualEndLocation = { x: h, y: l }, this.vx = (this.actualEndLocation.x - this.actualStartLocation.x) / s, this.vy = (this.actualEndLocation.y - this.actualStartLocation.y) / s; } get canvasSize() { return this.br.canvasSize; } } class _ { constructor(t) { r(this, "br"); // 用于维护当前渲染的顶部弹幕(从上往下排序) r(this, "topRenderBarrages", []); // 用于维护当前渲染的底部弹幕(从下往上排序) r(this, "bottomRenderBarrages", []); this.br = t; } /** * 获取当前应该渲染的固定弹幕 * @param allFixedBarrages 所有的固定弹幕数组 * @param time 视频播放的时间点 */ getRenderFixedBarrages(t, e) { const i = t.filter((h) => h.barrageType === "top" && e >= h.time && e <= h.endTime), s = t.filter((h) => h.barrageType === "bottom" && e >= h.time && e <= h.endTime); this.topRenderBarrages = this.topRenderBarrages.filter((h) => i.includes(h)), this.bottomRenderBarrages = this.bottomRenderBarrages.filter((h) => s.includes(h)); const a = i.filter((h) => !this.topRenderBarrages.includes(h)), o = s.filter((h) => !this.bottomRenderBarrages.includes(h)); return a.forEach((h) => { this.insertFixedBarrage(h); }), o.forEach((h) => { this.insertFixedBarrage(h); }), [...this.topRenderBarrages, ...this.bottomRenderBarrages]; } /** * 发送新的弹幕 * @param barrage 弹幕实例 */ send(t) { this.insertFixedBarrage(t); } /** * 封装通用的工具方法 * @param barrage 弹幕实例 */ insertFixedBarrage(t) { let e = !1; if (t.barrageType === "top") if (this.topRenderBarrages.length === 0) this.topRangeLength >= t.height && (t.top = this.topRange[0], this.topRenderBarrages.push(t), e = !0); else for (let i = 0; i < this.topRenderBarrages.length; i++) { const s = this.topRenderBarrages[i]; if (i === 0 && s.top - this.topRange[0] >= t.height) { t.top = this.topRange[0], this.topRenderBarrages.unshift(t), e = !0; break; } if ((i === this.topRenderBarrages.length - 1 ? this.topRange[1] - s.top - s.height : this.topRenderBarrages[i + 1].top - s.top - s.height) >= t.height) { t.top = s.top + s.height, this.topRenderBarrages.splice(i + 1, 0, t), e = !0; break; } } else if (this.bottomRenderBarrages.length === 0) this.bottomRangeLength >= t.height && (t.top = this.bottomRange[0] - t.height, this.bottomRenderBarrages.push(t), e = !0); else for (let i = 0; i < this.bottomRenderBarrages.length; i++) { const s = this.bottomRenderBarrages[i]; if (i === 0 && this.bottomRange[0] - s.top - s.height >= t.height) { t.top = this.bottomRange[0] - t.height, this.bottomRenderBarrages.unshift(t), e = !0; break; } if ((i === this.bottomRenderBarrages.length - 1 ? s.top - this.bottomRange[1] : s.top - this.bottomRenderBarrages[i + 1].top - this.bottomRenderBarrages[i + 1].height) >= t.height) { t.top = s.top - t.height, this.bottomRenderBarrages.splice(i + 1, 0, t), e = !0; break; } } t.prior && !e && (t.barrageType === "top" ? (t.top = c.Math.getRandomInt(this.topRange[0], this.topRange[1] - t.height), this.topRenderBarrages.push(t), this.topRenderBarrages.sort((i, s) => i.top - s.top)) : (t.top = c.Math.getRandomInt(this.bottomRange[1], this.bottomRange[0] - t.height), this.bottomRenderBarrages.push(t), this.bottomRenderBarrages.sort((i, s) => s.top - i.top))); } /** * 清空缓存数组 */ clearStoredBarrage() { this.topRenderBarrages = [], this.bottomRenderBarrages = []; } /** * 一半的 Canvas 高度,top 弹幕只能在 halfCanvasHeight 的上面,bottom 弹幕只能在 halfCanvasHeight 的下面 */ get middleHeightPoint() { return this.br.canvasSize.height / 2; } /** * 顶部弹幕 y 轴方向的范围 */ get topRange() { return [0, this.middleHeightPoint]; } /** * topRange 的长度 */ get topRangeLength() { return this.topRange[1] - this.topRange[0]; } /** * 底部弹幕 y 轴方向的范围 */ get bottomRange() { return [this.br.canvasSize.height, this.middleHeightPoint]; } /** * bottomRange 的长度 */ get bottomRangeLength() { return this.bottomRange[0] - this.bottomRange[1]; } } class z { constructor(t) { // 全局弹幕渲染器 r(this, "br"); // 实际轨道数组 r(this, "realTracks", []); // 虚拟轨道数组 r(this, "virtualTracks", []); // 实际轨道的高度 r(this, "realTrackHeight"); // 实际轨道的数量 r(this, "realTrackNum"); // 最高弹幕所占虚拟轨道的 grade r(this, "maxGrade"); // 以空间换时间,性能优化 // key:任意虚拟轨道;value:包含 virtualTrack 内部任一实际轨道的虚拟轨道所组成的数组 r(this, "vtToVtsMap", /* @__PURE__ */ new Map()); r(this, "gradeToVtsMap", /* @__PURE__ */ new Map()); this.br = t; } /** * 根据 br 的数据初始化实际轨道和虚拟轨道 * @param realTrackHeight 实际轨道高度 */ initTracks(t) { this.resetTracks(); const e = Math.floor(this.br.canvasSize.height * this.br.renderConfig.renderRegion / t); this.realTrackHeight = t, this.realTrackNum = e; for (let a = 1; a <= e; a++) this.realTracks.push(new P(a, t)); const i = this.realTracks.map((a) => a.id); let s = 1; for (let a = 1; a <= e; a++) { const o = e - (a - 1); for (let h = 1; h <= o; h++) this.virtualTracks.push( new N( s++, i.slice(h - 1, h - 1 + a), this.realTracks.slice(h - 1, h - 1 + a) ) ); } this.isLogKeyData && console.table([ { item: "实际轨道高度", value: t }, { item: "实际轨道数量", value: this.realTracks.length }, { item: "虚拟轨道数量", value: this.virtualTracks.length } ]), this.virtualTracks.forEach((a) => { this.vtToVtsMap.set(a, this.virtualTracks.filter((o) => o.grade <= this.maxGrade && a.rtIdArr.some((h) => o.rtIdSet.has(h)))); }); for (let a = 1; a <= e; a++) this.gradeToVtsMap.set(a, this.virtualTracks.filter((o) => o.grade === a)); } /** * 对滚动弹幕进行布局计算 * @param scrollBarrages 滚动弹幕实例数组 */ layoutScrollBarrages(t) { if (t.length === 0) return; const e = c.Math.findMode(t.map((i) => Math.ceil(i.height))); this.maxGrade = Math.ceil(Math.max(...t.map((i) => i.height)) / e), (!this.realTracks.length || !this.virtualTracks.length) && this.initTracks(e), t.forEach((i) => { i.grade = Math.ceil(i.height / e); }), this.avoidOverlap ? this.avoidOverlapLayout(t) : this.allowOverlapLayout(t); } /** * 进行不允许重叠的布局 * @param scrollBarrages 滚动弹幕实例数组 */ avoidOverlapLayout(t) { const e = Date.now(); this.virtualTracks.forEach((i) => i.clearBarrage()), t.forEach((i) => { const s = this.gradeToVtsMap.get(i.grade) || []; for (let a = 0; a < s.length; a++) { const o = s[a]; if ((this.vtToVtsMap.get(o) || []).every((l) => { const g = l.getLastBarrage(); return g ? g.originalRight + this.minSpace <= i.originalLeft : !0; })) { i.show = !0, o.push(i), i.top = o.top; break; } else i.show = !1; } i.prior && !i.show && this.randomTrackBarrage(i); }), this.isLogKeyData && console.log(`虚拟轨道算法花费时间:${Date.now() - e}ms`); } /** * 进行允许重叠的布局 * @param scrollBarrages 滚动弹幕实例数组 */ allowOverlapLayout(t) { t.forEach((e) => { this.randomTrackBarrage(e); }); } /** * 不允许重叠,插入新的弹幕 * @param scrollBarrage 滚动弹幕实例 */ avoidOverlapInsert(t) { let e = !1; const i = this.gradeToVtsMap.get(t.grade) || []; for (let s = 0; s < i.length; s++) { const a = i[s]; if (a.isEmpty) { t.show = !0, a.push(t), t.top = a.top, e = !0; break; } else if (t.originalLeft < a.getByIndex(0).originalLeft) { if (t.originalRight + this.minSpace < a.getByIndex(0).originalLeft) { t.show = !0, a.barrages.unshift(t), t.top = a.top, e = !0; break; } } else { const o = a.barrages.findIndex((h, l, g) => { const d = g[l + 1]; return h.originalLeft < t.originalLeft && (!d || t.originalLeft < d.originalLeft); }); if (o !== -1) { const h = a.barrages[o], l = a.barrages[o + 1]; if (h.originalRight + this.minSpace < t.originalLeft && (!l || t.originalRight + this.minSpace < l.originalLeft)) { t.show = !0, a.barrages.splice(o + 1, 0, t), t.top = a.top, e = !0; break; } } } } e || this.randomTrackBarrage(t); } /** * 发送新的弹幕 * @param scrollBarrage 滚动弹幕实例 */ send(t) { t.grade = Math.ceil(t.height / this.realTrackHeight), this.br.renderConfig.avoidOverlap ? this.avoidOverlapInsert(t) : this.randomTrackBarrage(t); } /** * 重置存放 实际轨道 和 虚拟轨道 的数组 */ resetTracks() { this.realTracks = [], this.virtualTracks = [], this.vtToVtsMap.clear(), this.gradeToVtsMap.clear(); } /** * 处理高度变化重新进行布局计算 * @param scrollBarrages 滚动弹幕实例数组 */ heightChangeReLayoutCalc(t) { this.resetTracks(), this.layoutScrollBarrages(t); } /** * 随机一个实际轨道并设置弹幕 * @param barrage 滚动弹幕实例 */ randomTrackBarrage(t) { const e = this.getRandomRealTrack(); t.top = e.top, t.show = !0; } /** * 是否允许弹幕相互重叠 */ get avoidOverlap() { return this.br.renderConfig.avoidOverlap; } /** * 是否打印关键数据 */ get isLogKeyData() { return this.br.devConfig.isLogKeyData; } /** * 获取一个随机的实际轨道 */ getRandomRealTrack() { return this.realTracks[c.Math.getRandomInt(0, this.realTracks.length - 1)]; } /** * 获取滚动弹幕的最小间距 */ get minSpace() { return this.br.renderConfig.minSpace; } } class P { constructor(t, e) { // 实际轨道的唯一 id r(this, "id"); // 实际轨道的高度 r(this, "height"); this.id = t, this.height = e; } /** * 当前实际轨道的 top */ get top() { return (this.id - 1) * this.height; } } class N { constructor(t, e, i) { // 虚拟轨道的唯一 id r(this, "id"); // 当前虚拟轨道包含的实际轨道,数组形式 r(this, "rtIdArr"); r(this, "rtIdSet"); r(this, "rtInstanceArr"); // 当前虚拟轨道包含的滚动弹幕 r(this, "barrages", []); this.id = t, this.rtIdArr = e, this.rtIdSet = new Set(e), this.rtInstanceArr = i; } /** * 获取轨道中的最后一个滚动弹幕 */ getLastBarrage() { return this.barrages[this.barrages.length - 1]; } /** * 向虚拟轨道中添加新的滚动弹幕 * @param barrage */ push(t) { this.barrages.push(t); } /** * 清空所有的弹幕 */ clearBarrage() { this.barrages = []; } /** * 获取指定下标的滚动弹幕 * @param index */ getByIndex(t) { return this.barrages[t]; } /** * 当前虚拟轨道的级别(包含虚拟轨道的数量) */ get grade() { return this.rtIdArr.length; } /** * 当前弹幕是不是空的 */ get isEmpty() { return this.barrages.length === 0; } /** * 当前虚拟轨道的 top */ get top() { return this.rtInstanceArr[0].top; } } class X { constructor({ barrageRenderer: t }) { r(this, "br"); r(this, "allBarrageInstances", []); r(this, "fixedBarrageInstances", []); r(this, "scrollBarrageInstances", []); r(this, "seniorBarrageInstances", []); // 固定弹幕布局计算器 r(this, "fixedBarrageLayout"); r(this, "virtualTrackAlgorithm"); this.br = t, this.fixedBarrageLayout = new _(this.br), this.virtualTrackAlgorithm = new z(this.br); } /** * 设置弹幕数据 * @param barrageOptions 弹幕配置数组 */ setBarrages(t) { let e = t.map((i) => { switch (i.barrageType) { case "top": case "bottom": return new B(i, this.br); case "scroll": return new T(i, this.br); case "senior": return new y(i, this.br); } }); e = e.sort((i, s) => i.time - s.time), this.allBarrageInstances = e, this.scrollBarrageInstances = e.filter((i) => i.barrageType === "scroll"), this.fixedBarrageInstances = e.filter((i) => ["top", "bottom"].includes(i.barrageType)), this.seniorBarrageInstances = e.filter((i) => i.barrageType === "senior"), this.virtualTrackAlgorithm.layoutScrollBarrages(this.scrollBarrageInstances); } /** * 获取某一时刻需要渲染的弹幕,交由外部进行渲染 * @param time 视频播放时间点 */ getRenderBarrages(t) { const e = this.getRenderScrollBarrages(t), i = this.getRenderFixedBarrages(t), s = this.getRenderSeniorBarrages(t); return [ ...e, ...i, ...s ].sort((a, o) => a.prior !== o.prior ? a.prior ? 1 : -1 : a.time - o.time); } /** * 发送新的弹幕 * @param barrage 弹幕配置对象 */ send(t) { if (t.barrageType === "scroll") { const e = new T(t, this.br); c.Algorithm.insertBarrageByTime(this.scrollBarrageInstances, e), this.virtualTrackAlgorithm.send(e); } else if (t.barrageType === "top" || t.barrageType === "bottom") { const e = new B(t, this.br); c.Algorithm.insertBarrageByTime(this.fixedBarrageInstances, e), this.fixedBarrageLayout.send(e); } else if (t.barrageType === "senior") { const e = new y(t, this.br); c.Algorithm.insertBarrageByTime(this.seniorBarrageInstances, e); } } /** * 获取当前应该渲染的滚动弹幕 * @param time 视频播放时间点 */ getRenderScrollBarrages(t) { const e = t / 1e3 * this.br.renderConfig.speed, i = this.scrollBarrageInstances.filter((s) => s.show && s.top !== void 0).filter( (s) => s.originalRight - e >= 0 && s.originalLeft - e < this.br.canvasSize.width ); return i.forEach((s) => { s.left = s.originalLeft - e; }), i; } /** * 获取当前应该渲染的固定弹幕 * @param time 视频播放时间点 */ getRenderFixedBarrages(t) { return this.fixedBarrageLayout.getRenderFixedBarrages(this.fixedBarrageInstances, t); } /** * 获取当前应该渲染的高级弹幕 * @param time 视频播放时间点 */ getRenderSeniorBarrages(t) { const e = this.seniorBarrageInstances.filter( (i) => ( // 当前时间大于等于弹幕的出现时间 并且 当前时间小于等于弹幕的结束时间 t >= i.time && t <= i.time + i.seniorBarrageConfig.totalDuration ) ); return e.forEach((i) => { const s = i.time, a = s + i.seniorBarrageConfig.delay, o = a + i.seniorBarrageConfig.motionDuration; if (t >= s && t <= a) i.left = i.actualStartLocation.x, i.top = i.actualStartLocation.y; else if (t >= a && t <= o) { const h = t - a; i.left = i.actualStartLocation.x + h * i.vx, i.top = i.actualStartLocation.y + h * i.vy; } else i.left = i.actualEndLocation.x, i.top = i.actualEndLocation.y; }), e; } /** * 处理宽度 change */ handleWidthChange() { this.fixedBarrageInstances.forEach((t) => t.calcFixedBarrageLeft()), this.scrollBarrageInstances.forEach((t) => t.calcOriginal()); } /** * 处理高度 change */ handleHeightChange() { this.fixedBarrageLayout.clearStoredBarrage(), this.virtualTrackAlgorithm.heightChangeReLayoutCalc(this.scrollBarrageInstances); } /** * 尺寸发生变化的时候调用,会重新计算内部数据 * @param type 尺寸变化的类型 */ resize(t) { this.seniorBarrageInstances.forEach((e) => e.calcActualLocation()), t === "ONLY_WIDTH" ? this.handleWidthChange() : t === "ONLY_HEIGHT" ? this.handleHeightChange() : (this.handleWidthChange(), this.handleHeightChange()); } /** * 根据 render Config change 进行布局方面的重新计算 * @param isSpeedChange 重新计算 originalLeft,如果当前是不允许遮挡的话,重新进行虚拟轨道计算; * @param isHeightReduceChange 重置轨道数据,根据 avoidOverlap 进行重新布局,清空固定弹幕的 store * @param isRenderRegionChange 重置轨道数据,根据 avoidOverlap 进行重新布局 * @param isAvoidOverlapChange 根据 avoidOverlap 进行重新布局 * @param isMinSpaceChange 如果当前是不允许遮挡的话,重新进行虚拟轨道计算; */ renderConfigChange(t, e, i, s, a) { t && this.scrollBarrageInstances.forEach((o) => o.calcOriginal()), e && this.fixedBarrageLayout.clearStoredBarrage(), (e || i) && this.virtualTrackAlgorithm.resetTracks(), (t && this.br.renderConfig.avoidOverlap || e || i || s || a && this.br.renderConfig.avoidOverlap) && this.virtualTrackAlgorithm.layoutScrollBarrages(this.scrollBarrageInstances); } } var u = /* @__PURE__ */ ((n) => (n[n.FIXED_DURATION_ERROR = 1] = "FIXED_DURATION_ERROR", n[n.SENIOR_TOTAL_ERROR = 2] = "SENIOR_TOTAL_ERROR", n[n.SENIOR_DELAY_ERROR = 3] = "SENIOR_DELAY_ERROR", n[n.SENIOR_MOTION_ERROR = 4] = "SENIOR_MOTION_ERROR", n))(u || {}); class m extends Error { constructor(e) { super(e.message); r(this, "code"); this.code = e.code; } } class Y { constructor({ container: t, video: e, barrages: i, barrageImages: s, renderConfig: a, devConfig: o }) { // 容器 DOM r(this, "container"); // video 元素 r(this, "video"); // Canvas 元素 r(this, "canvas"); // Canvas 渲染上下文; r(this, "ctx"); // 弹幕中渲染图片的配置 r(this, "barrageImages"); // 默认渲染配置 r(this, "defaultRenderConfig", { heightReduce: 0, speed: 200, opacity: 1, renderRegion: 1, fontFamily: "Microsoft YaHei", fontWeight: "normal", avoidOverlap: !0, minSpace: 10 }); // 渲染配置 r(this, "renderConfig", this.defaultRenderConfig); // 默认开发配置 r(this, "defaultDevConfig", { isRenderFPS: !1, isRenderBarrageBorder: !1, isLogKeyData: !1 }); // 开发相关配置 r(this, "devConfig", this.defaultDevConfig); // 弹幕布局计算器 r(this, "barrageLayoutCalculate", new X({ barrageRenderer: this })); // 用于标识弹幕功能是否被打开 r(this, "isOpen", !0); r(this, "animationHandle"); r(this, "fps", ""); r(this, "lastFrameTime"); r(this, "lastCalcTime", 0); // 记录上次布局计算时,container 的宽高 r(this, "lastContainerSize", { width: 0, height: 0 }); // 离屏 canvas 优化 r(this, "offscreenCanvas"); r(this, "offscreenCanvasCtx"); r(this, "dpr", c.Canvas.getDevicePixelRatio()); this.video = e, this.setRenderConfigInternal(a || {}, !0), this.setDevConfig(o || {}), this.barrageImages = s, this.container = typeof t == "string" ? document.getElementById(t) : t, this.canvas = document.createElement("canvas"), this.ctx = this.canvas.getContext("2d"), this.offscreenCanvas = document.createElement("canvas"), this.offscreenCanvasCtx = this.offscreenCanvas.getContext("2d"), this.handleDOM(this.container, this.canvas, this.ctx), this.setBarrages(i), this.devConfig.isLogKeyData && console.log("全局实例:", this); } /** * 处理 DOM 相关 * @param container 容器 DOM 元素 * @param canvas canvas 元素 * @param ctx 渲染上下文 */ handleDOM(t, e, i) { t || console.error("Unable to obtain container element"), i || console.error("Unable to obtain CanvasRenderingContext2D"), !(!t || !i) && (t.style.position = "relative", e.style.position = "absolute", e.style.left = "0px", e.style.top = "0px", e.style.pointerEvents = "none", e.width = t.clientWidth, e.height = t.clientHeight - (this.renderConfig.heightReduce ?? 0), t.appendChild(e), this.handleHighDprVague(e, i), this.offscreenCanvas.width = t.clientWidth, this.offscreenCanvas.height = t.clientHeight - (this.renderConfig.heightReduce ?? 0), this.handleHighDprVague(this.offscreenCanvas, this.offscreenCanvasCtx)); } /** * 处理 Canvas 在高分屏上渲染模糊的问题 */ handleHighDprVague(t, e) { const i = t.width, s = t.height; t.width = i * this.dpr, t.height = s * this.dpr, t.style.width = i + "px", t.style.height = s + "px", e.scale(this.dpr, this.dpr), e.textBaseline = "hanging"; } /** * 发送新的弹幕 * @param barrage 弹幕配置对象 */ send(t) { const e = this.validateBarrageOption(t); if (e !== !0) throw e; t.prior = !0, this.barrageLayoutCalculate.send(t); } /** * container 元素尺寸变更后,调用进行重新计算 */ resize() { var o, h; this.handleDOM(this.container, this.canvas, this.ctx); const t = { width: ((o = this.container) == null ? void 0 : o.clientWidth) || 0, height: ((h = this.container) == null ? void 0 : h.clientHeight) || 0 }, { width: e, height: i } = this.lastContainerSize, s = e !== t.width, a = i !== t.height; (s || a) && (this.lastContainerSize = t, s && !a ? this.barrageLayoutCalculate.resize("ONLY_WIDTH") : !s && a ? this.barrageLayoutCalculate.resize("ONLY_HEIGHT") : this.barrageLayoutCalculate.resize("BOTH")), this.renderFrame(); } /** * 设置弹幕数据 * @param barrages 弹幕配置对象数组 */ setBarrages(t) { var e, i; t && (t = t.filter((s) => this.validateBarrageOption(s) === !0), this.barrageLayoutCalculate.setBarrages(t), this.lastContainerSize = { width: ((e = this.container) == null ? void 0 : e.clientWidth) || 0, height: ((i = this.container) == null ? void 0 : i.clientHeight) || 0 }); } /** * 设置渲染配置(可以部分设置配置) * @param renderConfig 配置对象 * @param init 是不是初始化 */ setRenderConfigInternal(t, e = !1) { const i = Object.keys(t), s = i.includes("speed") && t.speed !== this.renderConfig.speed, a = i.includes("heightReduce") && t.heightReduce !== this.renderConfig.heightReduce, o = i.includes("renderRegion") && t.renderRegion !== this.renderConfig.renderRegion, h = i.includes("avoidOverlap") && t.avoidOverlap !== this.renderConfig.avoidOverlap, l = i.includes("minSpace") && t.minSpace !== this.renderConfig.minSpace; Object.assign(this.renderConfig, t), !e && (s || a || o || h) && (a && this.handleDOM(this.container, this.canvas, this.ctx), this.barrageLayoutCalculate.renderConfigChange( s, a, o, h, l )), !this.animationHandle && !e && this._render(); } /** * 设置渲染配置(可以部分设置配置) * @param renderConfig 渲染配置 */ setRenderConfig(t) { this.setRenderConfigInternal(t); } /** * 设置开发配置(可以部分设置配置) * @param devConfig 开发配置 */ setDevConfig(t) { Object.assign(this.devConfig, t); } /** * 负责每一帧的渲染 * @private */ _render() { if (!this.isOpen) return; let t = this.barrageLayoutCalculate.getRenderBarrages(this.progress); this.renderConfig.barrageFilter && (t = t.filter((e) => this.renderConfig.barrageFilter(e))), this.offscreenCanvasCtx.clearRect(0, 0, this.offscreenCanvas.width, this.offscreenCanvas.height), this.offscreenCanvasCtx.globalAlpha = this.renderConfig.opacity, t.forEach((e) => { e.render(this.offscreenCanvasCtx); }), this.devConfig.isRenderFPS && this.renderFps(), this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height), this.offscreenCanvas.width && this.ctx.drawImage( this.offscreenCanvas, 0, 0, this.offscreenCanvas.width, this.offscreenCanvas.height, 0, 0, this.canvas.width / this.dpr, this.canvas.height / this.dpr ), this.animationHandle && requestAnimationFrame(() => this._render()); } /** * 创建动画任务 * @private */ _createAnimation() { !this.animationHandle && this.isOpen && (this.animationHandle = requestAnimationFrame(() => this._render())); } /** * 当前动画的播放进度,单位:毫秒 */ get progress() { return this.videoStatus.currentTime; } /** * video 的状态 */ get videoStatus() { return { // 当前视频的播放进度(ms) currentTime: this.video.currentTime * 1e3, // 当前视频是不是播放中 playing: !this.video.paused }; } /** * canvas 的尺寸 */ get canvasSize() { return { width: this.canvas.width / this.dpr, height: this.canvas.height / this.dpr }; } /** * 触发一帧的渲染 */ renderFrame() { this.animationHandle || this._render(); } /** * 执行弹幕的播放 */ play() { this._createAnimation(); } /** * 暂停弹幕的播放 */ pause() { this.animationHandle && cancelAnimationFrame(this.animationHandle), this.animationHandle = void 0; } /** * 是否打开弹幕 * @param isOpen 是否打开弹幕 */ switch(t) { this.isOpen = t, t ? this.videoStatus.playing ? this._createAnimation() : this._render() : (this.animationHandle && cancelAnimationFrame(this.animationHandle), this.animationHandle = void 0, this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)); } /** * 渲染 FPS */ renderFps() { const t = Date.now(); this.lastFrameTime && t - this.lastCalcTime > 200 && (this.fps = `${Math.floor(1e3 / (t - this.lastFrameTime))}FPS`, this.lastCalcTime = t), this.lastFrameTime = t, this.fps && (this.offscreenCanvasCtx.font = "bold 32px Microsoft YaHei", this.offscreenCanvasCtx.fillStyle = "blue", this.offscreenCanvasCtx.fillText(this.fps, 20, 30)); } /** * 判断弹幕数据是否合规 * @param barrage 弹幕配置对象 */ validateBarrageOption(t) { if ((t.barrageType === "top" || t.barrageType === "bottom") && t.duration <= 0) return new m({ code: u.FIXED_DURATION_ERROR, message: "The duration of the fixed barrage should be greater than 0" }); if (t.barrageType === "senior") { const { totalDuration: e, delay: i, motionDuration: s } = t.seniorBarrageConfig; if (e <= 0) return new m({ code: u.SENIOR_TOTAL_ERROR, message: "The totalDuration of senior barrage should be greater than 0" }); if (i < 0) return new m({ code: u.SENIOR_DELAY_ERROR, message: "The delay of senior barrage should be greater than or equal to 0" }); if (s < 0) return new m({ code: u.SENIOR_MOTION_ERROR, message: "The motionDuration of senior barrage should be greater than or equal to 0" }); } return !0; } } export { m as BarrageOptionError, Y as BarrageRenderer, v as BaseBarrage, u as ErrorCode, B as FixedBarrage, A as ImageSection, T as ScrollBarrage, y as SeniorBarrage, M as TextSection, Y as default };