"use strict"; function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t["return"] || t["return"](); } finally { if (u) throw o; } } }; } function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); } function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); } function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } } function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } var YbDanmu = /*#__PURE__*/function () { function YbDanmu(container, data) { var config = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; _classCallCheck(this, YbDanmu); this.container = typeof container == 'string' ? document.querySelector(container) : container; this.config = _objectSpread(_objectSpread({}, YbDanmu.DEFAULT_CONFIG), config); this.data = data; this.filter = new YbDanmuFilter(this.config.filter); //过滤规则 this.list = this._filterData(data); this.runline = []; //正在跑动的弹幕列表 this.canvas = null; //canvas元素 this.ctx = null; //canvas实例 this.paused = true; //是否暂停 this.currentTime = 0; //当前时间轴 this._animation = null; //动画实例 } return _createClass(YbDanmu, [{ key: "load", value: //加载弹幕 function load() { if (this.container) { var canvas = document.createElement('CANVAS'); this.container.appendChild(canvas); this.canvas = canvas; this.ctx = canvas.getContext('2d'); this.updateSize(); } } //卸载弹幕 }, { key: "unload", value: function unload() { this._clearAnimationFrame(); this.list = []; this.runline = []; if (this.ctx) this.ctx = null; if (this.canvas) { this.canvas.remove(); this.canvas = null; } } //更新数据 }, { key: "setData", value: function setData(data) { this.data = data; this.reset(); } //动态配置 }, { key: "setConfig", value: function setConfig(key, value) { this.config[key] = value; } //播放弹幕 }, { key: "play", value: function play() { this.paused = false; this._render(); } //暂停弹幕 }, { key: "pause", value: function pause() { this._clearAnimationFrame(); this.paused = true; } //跳转弹幕 }, { key: "seek", value: function seek(time) { this.currentTime = time; this.reset(); } //更新时间轴(需要外部更新时间轴否则弹幕不会运行) }, { key: "time", value: function time(_time) { var _this = this; //计算本次更新时间和上次时间的时间差 var diffrenece = _time - this.currentTime; //记录新时间 this.currentTime = _time; //新列表 var newList = []; //跑动列表 var runline = []; //循环弹幕列表 this.list.forEach(function (danmu) { //当前时间减去弹幕显示时间 var range = _this.getCurrentTime() - danmu.time; //获取当前时间和上次时间的时间差以内的弹幕 if (range >= 0 && range < Math.abs(diffrenece)) runline.push(danmu); //记录不在范围内的弹幕 else newList.push(danmu); }); //去掉已经渲染的弹幕 this.list = newList; //计算需要渲染弹幕的布局 for (var i = 0; i < runline.length; i++) { var bar = this._layout(runline[i]); if (bar) this.runline.push(bar); } //排序弹幕 this.runline.sort(function (a, b) { return a.time - b.time; }); } //清空画布 }, { key: "clear", value: function clear() { this.ctx && this.ctx.clearRect(0, 0, this.canvas.offsetWidth, this.canvas.offsetHeight); } //显示弹幕舞台 }, { key: "show", value: function show() { this.container.style.visibility = 'visible'; } //隐藏弹幕舞台 }, { key: "hide", value: function hide() { this.container.style.visibility = 'hidden'; } //更新尺寸 }, { key: "updateSize", value: function updateSize() { var width = this.container.offsetWidth; var height = this.container.offsetHeight; var dpr = this.config.accuracy == 'auto' ? window.devicePixelRatio : this.config.accuracy; this.canvas.width = width * dpr; this.canvas.height = height * dpr; this.canvas.style.width = width; this.canvas.style.height = height; this.ctx.scale(dpr, dpr); } //重置数据和画布 }, { key: "reset", value: function reset() { var _this2 = this; this.clear(); this.list = this._filterData(this.data); this.runline = []; var duration = this.canvas.offsetWidth / (this.config.speed * this.config.playbackRate) / 60; //滚动周期(除以60是因为为大概每秒60帧)根据滚动周期去获取可能出现在屏幕上的弹幕 var newList = []; var runline = []; this.list.forEach(function (danmu) { var range = _this2.getCurrentTime() - danmu.time; //重置数据的时候,考虑到播放进度可能在中间,未避免弹幕丢失,将时间误差控制在滚动周期以内 if (range >= 0 && range < duration) runline.push(danmu); //记录不在范围内的弹幕 else newList.push(danmu); }); //去掉已经渲染的弹幕 this.list = newList; //计算渲染弹幕的布局 for (var i = 0; i < runline.length; i++) { var bar = this._layout(runline[i]); if (bar) this.runline.push(bar); } this.runline.sort(function (a, b) { return a.time - b.time; }); //排序弹幕 } }, { key: "refresh", value: function refresh() { this.updateSize(); this.reset(); } //弹幕是否正在显示 }, { key: "getVisible", value: function getVisible() { var _this$container$style; return this.container ? ((_this$container$style = this.container.style) === null || _this$container$style === void 0 ? void 0 : _this$container$style.visibility) == 'visible' : false; } //获取时间进度 }, { key: "getCurrentTime", value: function getCurrentTime() { return this.currentTime + this.config.timeDiffrence; //加上时间差 } /** * 发送弹幕 * @param {Object} danmu 弹幕数据 * @param {Boolean}border 使用边框 */ }, { key: "send", value: function send(danmu, border) { this.data.push(danmu); this.runline.push(this._layout(_objectSpread(_objectSpread({}, danmu), {}, { time: danmu.time + this.config.timeDiffrence }), border, true)); //强制显示弹幕 } //插入弹幕数据,但不会渲染 }, { key: "insert", value: function insert(danmu) { this.data.push(danmu); this.list.push(danmu); } //渲染弹幕 }, { key: "_render", value: function _render() { var _this3 = this; this._clearAnimationFrame(); if (this.paused) return; if (this.runline.length) { this.clear(); for (var i = 0; i < this.runline.length; i++) { var b = this.runline[i]; //移除渲染完毕的弹幕 if (b.left + b.width <= 0 || b.boxWidth <= 0) { this.runline.splice(i, 1); i--; continue; } // b.speed = this._detectionBump(b);//碰撞检测,保留位置,可能不需要 var speed = (this.config.speed + b.speed) * this.config.playbackRate; //弹幕随机的速度加上默认设置速度再乘以倍速,才是真正移动的速度 if (b.mode == 1 || !b.mode) b.left -= speed; //滚动弹幕将x轴位置减去速度,实现滚动动画 else b.boxWidth -= speed; //顶端和底端弹幕,将记录的画布宽度减去速度,实现渲染时长 //非滚动弹幕,当boxWidth大于画布宽度时,则不绘制,避免弹幕提前出现 if (b.mode == 1 || !b.mode || b.boxWidth <= this.canvas.offsetWidth) this._drawText(b); } } this._animation = window.requestAnimationFrame(function () { return _this3._render(); }); } /** * 计算弹幕布局 * @param { Object|String } danmu 弹幕对象 * @param {Boolean} border 是否使用边框 * @param {Boolean} force 是否强制显示 */ }, { key: "_layout", value: function _layout(danmu, border, force) { var ctx = this.ctx; var canvas = this.canvas; var config = this.config; var boxWidth = canvas.offsetWidth; var fontSize = danmu.fontSize || config.fontSize; var fontFamily = danmu.fontFamily || config.fontFamily; var fontScale = config.fontScale; var color = danmu.color || config.color; var mode = danmu.mode || 1; var text = danmu.text; var time = danmu.time; ctx.font = "".concat(fontSize * fontScale, "px ").concat(fontFamily); //设置字体大小和样式 var width = Math.ceil(ctx.measureText(text).width); //测算文本宽度 var speed = mode == 1 ? Math.random() * 0.1 : 0; //随机移动速度(弹幕一帧移动的距离,加上随机数,可以让弹幕滚动更加有层次感) var offset = (this.getCurrentTime() - time) * 60 * (config.speed + speed); //根据弹幕出现时间和当前时间轴的时间差,计算弹幕的初始偏移值 var left = mode == 1 ? boxWidth - offset : boxWidth / 2 - width / 2; //获取弹幕x轴位置 var track = this._getTrack(fontSize, mode, offset, force); //获取弹幕y轴位置 var top = track.top; if (top > -1) { return { mode: mode, text: text, time: time, fontSize: fontSize, color: color, top: top, left: left, speed: track.isOverlap ? speed + 2 + Math.random() * 0.5 : speed, //如果是重叠的弹幕则加快速度,避免一直重叠,这样观感不好 width: width, boxWidth: boxWidth - offset, //顶部和底部弹幕的显示时间根据舞台宽度和移动速度决定 border: border //自己发送的弹幕需要加上边框,作区别显示 }; } return false; } /** * 绘制文字 * @param {Object} danmu 弹幕对象 * @param {String} mode 弹幕模式 1-滚动 4-底部 5-顶部 */ }, { key: "_drawText", value: function _drawText(danmu) { var mode = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1; var ctx = this.ctx; var config = this.config; if (ctx) { ctx.beginPath(); var fontSize = danmu.fontSize || config.fontSize; var fontFamily = danmu.fontFamily || config.fontFamily; var color = danmu.color || config.color; var opacity = config.opacity; var fontScale = config.fontScale; var newFontSize = fontSize * fontScale; ctx.font = "".concat(fontSize * fontScale, "px ").concat(fontFamily); ctx.strokeStyle = YbDanmu.colorToRgba(YbDanmu.getStrokeColor(color), opacity); ctx.strokeText(danmu.text, danmu.left, danmu.top); ctx.fillStyle = YbDanmu.colorToRgba(color, opacity); ctx.fillText(danmu.text, danmu.left, danmu.top); if (danmu.border) { //如果带边框 ctx.strokeStyle = color; //边框颜色 ctx.lineWidth = 2; //边框宽度 ctx.strokeRect(danmu.left, danmu.top - newFontSize, danmu.width, newFontSize + config.lineGap); //绘制边框 } ctx.closePath(); } } /** * 计算弹幕应该放在哪条轨道 * @param fontSize(弹幕字体大小) * @param mode(弹幕模式)1-滚动 4-底部 5-顶部 * @param offset(弹幕初始x轴位置偏差值) * @param force(是否强制显示,用于弹幕发送) */ }, { key: "_getTrack", value: function _getTrack(fontSize) { var mode = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1; var offset = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; var force = arguments.length > 3 ? arguments[3] : undefined; //canvas绘制文字x,y坐标是按文字左下角计算,预留30px var canvas = this.canvas; var isOverlap = false; //是否重叠 var nowTop = -1; //最终返回的顶部距离 var minLen = -1; //最小弹幕数量 var minTop = -1; //最小弹幕数量轨道 var boxWidth = canvas.offsetWidth; var boxHeight = canvas.offsetHeight; var config = this.config; var lineGap = config.lineGap; var top = config.top; var bottom = config.bottom; var fontScale = config.fontScale; var newFontSize = fontSize * fontScale; var reserve = 5; //画布预留位置(给画布底部流出一段距离)不预留可能导致最后一个轨道的弹幕显示不全 var trackLen = Math.floor((boxHeight - top - bottom) / (newFontSize + lineGap)); //计算轨道数量 var tracks = []; if (mode == 4) { //弹幕模式为底端,从底部开始加载,所以要用画布高度减去轨道高度,才是底端弹幕渲染的轨道高度 for (var i = 0; i < trackLen; i++) tracks.push(boxHeight - (i * newFontSize + i * lineGap + top + reserve)); //排列轨道 } else { for (var _i = 0; _i < trackLen; _i++) tracks.push((_i + 1) * newFontSize + _i * lineGap + top); //排列轨道 } for (var _i2 = 0; _i2 < tracks.length; _i2++) { var trackTop = tracks[_i2]; //当前轨道上有多少条弹幕 var danmus = this.runline.filter(function (danmu) { return danmu.top < trackTop + newFontSize && danmu.fontSize * fontScale + danmu.top > trackTop && danmu.mode == mode; }); if (danmus.length < minLen || minLen == -1) { //获取最少弹幕数量的轨道 minLen = danmus.length; minTop = trackTop; } if (danmus.length == 0) { //当前轨道没有弹幕运行,直接插入该轨道 nowTop = trackTop; break; } else { if (danmus.length > 0 && mode == 1) { //当前轨道有弹幕运行,且弹幕模式是滚动模式时 var arr = danmus.map(function (danmu) { return danmu.left + danmu.width; }); var max = Math.max.apply(Math, _toConsumableArray(arr)); //获取当前轨道最右的弹幕 //如果当前轨道还有空位则将弹幕放入当前轨道 if (max < boxWidth - offset - 10) { //预留10像素间隔 nowTop = trackTop; break; } } } } if (nowTop == -1 && (!config.overlap || force)) { //没有找到可插入的轨道,但没有开启防重叠或者需要强制显示弹幕,这个弹幕弹幕会无视原来的弹幕开启新的弹幕墙 nowTop = minTop; //将弹幕强制插入最少弹幕数量的轨道 isOverlap = true; //重叠弹幕 } return { top: nowTop + top, //加上上边距top isOverlap: isOverlap }; } //过滤数据 }, { key: "_filterData", value: function _filterData(data) { var _this4 = this; var config = this.config; var disableScroll = config.disableScroll; var disableTop = config.disableTop; var disableBottom = config.disableBottom; return YbPlayer.deepClone(data.filter(function (item) { return (item.mode != 1 || !disableScroll) && (item.mode != 5 || !disableTop) && (item.mode != 4 || !disableBottom) && !_this4.filter.filter(item.text); })); } }, { key: "_clearAnimationFrame", value: function _clearAnimationFrame() { if (this._animation) { window.cancelAnimationFrame(this._animation); this._animation = null; } } }], [{ key: "parseBiliXml", value: //格式化b站XML文件弹幕 function parseBiliXml(content) { var pattern = /]*>(.*?)<\/d\s*>/g; var danmuku = []; var match = ''; while ((match = pattern.exec(content)) !== null) { var attr = match[0].match(/p=\"*([\s\S]*?)\"/)[1]; var attrs = attr.split(','); danmuku.push({ mode: attrs[1], time: Number(attrs[0]), color: YbDanmu.decimalToRgb(Number(attrs[3])), fontSize: attrs[2], text: match[1] }); } return danmuku; } //格式化b站ssa文件弹幕 }, { key: "parseBiliSsa", value: function parseBiliSsa(content) { var lines = content.split('\n'); var events = []; var currentSection; lines.forEach(function (line) { line = line.trim(); if (line.startsWith('[') && line.endsWith(']')) { currentSection = line.slice(1, -1).toLowerCase(); return; } if (currentSection === 'events') { if (line.toLowerCase().startsWith('format:')) { // 解析事件格式行 var format = line.slice(7).split(',').map(function (item) { return item.trim().toLowerCase(); }); // 存储事件格式定义 } else if (line.toLowerCase().startsWith('dialogue:')) { // 解析对话行 var dialogueValues = line.slice(10).split(',').map(function (item) { return item.trim(); }); // 注意:文本部分可能包含逗号,所以不能简单分割 var textIndex = 9; // 根据format,文本通常是第9个字段(从0开始计数) var text = dialogueValues.slice(9).join(','); // 合并文本部分 var dialogueData = { start: dialogueValues[1], end: dialogueValues[2], style: dialogueValues[3], text: text }; events.push(dialogueData); } } }); return events.filter(function (e) { return !e.text.includes('\\a5\\pos'); }).map(function (e) { var style = e.text.match(/\{*([\s\S]*?)\}/)[1]; var arr = style.split('\\'); return { mode: 1, time: timeToSeconds(e.start), color: arr[2].replace('c&H', '#'), fontSize: arr[3].replace('fs', ''), text: e.text.replace(/\{.*?\}/g, '') }; }); } //根据文字颜色自动生成反色边框 }, { key: "getStrokeColor", value: function getStrokeColor(color) { var r, g, b; if (color.startsWith('#')) { color = color.length == 7 ? color : '#' + color.slice(1, 4) + color.slice(1, 4); r = parseInt(color.slice(1, 3), 16); g = parseInt(color.slice(3, 5), 16); b = parseInt(color.slice(5, 7), 16); } if (color.startsWith('rgb')) { var match = color.match(/rgb\(*([\s\S]*?)\)/); var colors = match[1].split(','); r = colors[0]; g = colors[1]; b = colors[2]; } var $grayLevel = r * 0.299 + g * 0.587 + b * 0.144 / 255; //判断是否是深色 if ($grayLevel < 0.5) return '#ffffff';else return '#000000'; } }, { key: "colorToRgba", value: function colorToRgba(color, opacity) { if (color.startsWith('#')) { color = color.length == 7 ? color : '#' + color.slice(1, 4) + color.slice(1, 4); var _str = "rgba("; var _r = parseInt(color.slice(1, 3), 16).toString(); var _g = parseInt(color.slice(3, 5), 16).toString(); var _b = parseInt(color.slice(5, 7), 16).toString(); _str += _r + "," + _g + "," + _b + "," + opacity + ")"; return _str; } if (color.startsWith('rgb')) { var str = "rgba("; var match = color.match(/rgb\(*([\s\S]*?)\)/); var colors = match[1].split(','); var r = colors[0]; var g = colors[1]; var b = colors[2]; str += r + "," + g + "," + b + "," + opacity + ")"; return str; } } //16进制转rgb }, { key: "decimalToRgb", value: function decimalToRgb(decimal) { var r = decimal >> 16 & 255; var g = decimal >> 8 & 255; var b = decimal & 255; return 'rgb(' + r + ',' + g + ',' + b + ')'; } }]); }(); //弹幕过滤 _defineProperty(YbDanmu, "DEFAULT_CONFIG", { accuracy: 1, //绘制精度 auto-自动使用屏幕DPI 任意整数-自定义精度 高精度会导致性能问题,谨慎使用 speed: 1, //弹幕运行速度 playbackRate: 1, //弹幕倍速 color: '#FFFFFF', //弹幕默认颜色 fontSize: 18, //弹幕默认大小 fontScale: 1, //弹幕规格(多少倍大小) fontFamily: 'Microsoft Yahei', //弹幕默认字体 opacity: 1, //弹幕透明度 top: 0, //舞台顶部间距 bottom: 0, //舞台底部间距 lineGap: 5, //弹幕行间距 overlap: false, //开启防重叠 timeDiffrence: 0, //时间差(用于校准弹幕和视频进度的时间差,一般不需要) disableScroll: false, //关闭滚动弹幕 disableTop: false, //关闭顶端弹幕 disableBottom: false, //关闭底端弹幕 disableFilter: false, //关闭弹幕过滤 filter: [] //过滤列表 }); var YbDanmuFilter = /*#__PURE__*/function () { function YbDanmuFilter(list) { _classCallCheck(this, YbDanmuFilter); this.rules = list || []; } // 添加 return _createClass(YbDanmuFilter, [{ key: "add", value: function add(rule) { //判断是否存在同类型的相同规则 var index = this.rules.findIndex(function (r) { return r.pattern == rule.pattern && r.type == rule.type; }); if (index > -1) return false; //如果已经存在返回false,表示插入失败 this.rules.push(rule); return true; //添加成功返回true } // 删除 }, { key: "remove", value: function remove(index) { this.rules.splice(index, 1); } // 过滤弹幕 }, { key: "filter", value: function filter(danmu) { var _iterator = _createForOfIteratorHelper(this.rules), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var rule = _step.value; if (rule.type === 'string') { // 字符串匹配 if (danmu.includes(rule.pattern)) { return true; } } else if (rule.type === 'regex') { // 正则表达式匹配 try { var regex = new RegExp(rule.pattern); if (regex.test(danmu)) { return true; } } catch (e) { console.error('无效的正则表达式:', rule.pattern, e); } } } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } return false; } // 获取所有规则 }, { key: "getRules", value: function getRules() { return this.rules; } }]); }(); //兼容new Function,为了挂载到window对象上 if (typeof window != 'undefined') { window.YbDanmu = YbDanmu; }