/*! * artplayer-plugin-danmuku.js v5.3.0 * Github: https://github.com/zhw2590582/ArtPlayer * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ function getMode(key) { switch (key) { case 1: case 2: case 3: return 0; case 4: return 2; case 5: return 1; default: return 0; } } function bilibiliDanmuParseFromXml(xmlString) { if (typeof xmlString !== "string") return []; const reg = /]*?p="(?

[^"]+)"[^>]*>(?.*?)<\/d>/gs; const matches = xmlString.matchAll(reg); return Array.from(matches).map((match) => { const attr = match.groups.p.split(","); if (attr.length >= 8) { const text = match.groups.text.trim().replaceAll(""", '"').replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">").replaceAll("&", "&"); return { text, time: Number(attr[0]), mode: getMode(Number(attr[1])), fontSize: Number(attr[2]), color: `#${Number(attr[3]).toString(16)}`, timestamp: Number(attr[4]), pool: Number(attr[5]), userID: attr[6], rowID: Number(attr[7]) }; } else { return null; } }).filter(Boolean); } function onmessage({ data }) { const { xml, id } = data; if (!id || !xml) return; const danmus = bilibiliDanmuParseFromXml(xml); globalThis.postMessage({ danmus, id }); } function createWorker() { const workerText = ` ${getMode.toString()} ${bilibiliDanmuParseFromXml.toString()} onmessage = ${onmessage.toString()} `; const blob2 = new Blob([workerText], { type: "application/javascript" }); return new Worker(URL.createObjectURL(blob2)); } function bilibiliDanmuParseFromUrl(url) { return new Promise(async (resolve) => { const res = await fetch(url); const xml = await res.text(); try { const worker = createWorker(); worker.onmessage = (event) => { const { danmus, id } = event.data; if (!id || !danmus) return; resolve(danmus); worker.terminate(); }; worker.postMessage({ xml, id: Date.now() }); } catch (error) { console.error("Error parsing Bilibili Danmu:", error); const danmus = bilibiliDanmuParseFromXml(xml); resolve(danmus); } }); } const jsContent = '/*!\n * artplayer-plugin-danmuku.js v5.3.0\n * Github: https://github.com/zhw2590582/ArtPlayer\n * (c) 2017-2026 Harvey Zhao\n * Released under the MIT License.\n */\nfunction getDanmuTop({ target, visibles, clientWidth, clientHeight, marginBottom, marginTop, antiOverlap }) {\n const maxTop = clientHeight - marginBottom;\n const danmus = visibles.filter((item) => item.mode === target.mode && item.top <= maxTop).sort((prev, next) => prev.top - next.top);\n if (danmus.length === 0) {\n if (target.mode === 2) {\n return maxTop - target.height;\n } else {\n return marginTop;\n }\n }\n danmus.unshift({\n type: "top",\n top: 0,\n left: 0,\n right: 0,\n height: marginTop,\n width: clientWidth,\n speed: 0,\n distance: clientWidth\n });\n danmus.push({\n type: "bottom",\n top: maxTop,\n left: 0,\n right: 0,\n height: marginBottom,\n width: clientWidth,\n speed: 0,\n distance: clientWidth\n });\n if (target.mode === 2) {\n for (let index = danmus.length - 2; index >= 0; index -= 1) {\n const item = danmus[index];\n const prev = danmus[index + 1];\n const itemBottom = item.top + item.height;\n const diff = prev.top - itemBottom;\n if (diff >= target.height) {\n return prev.top - target.height;\n }\n }\n } else {\n for (let index = 1; index < danmus.length; index += 1) {\n const item = danmus[index];\n const prev = danmus[index - 1];\n const prevBottom = prev.top + prev.height;\n const diff = item.top - prevBottom;\n if (diff >= target.height) {\n return prevBottom;\n }\n }\n }\n const topMap = [];\n for (let index = 1; index < danmus.length - 1; index += 1) {\n const item = danmus[index];\n if (topMap.length) {\n const last = topMap[topMap.length - 1];\n if (last[0].top === item.top) {\n last.push(item);\n } else {\n topMap.push([item]);\n }\n } else {\n topMap.push([item]);\n }\n }\n if (antiOverlap) {\n switch (target.mode) {\n case 0: {\n const result = topMap.find((list) => {\n return list.every((danmu) => {\n if (clientWidth < danmu.distance)\n return false;\n if (target.speed < danmu.speed)\n return true;\n const overlapTime = danmu.right / (target.speed - danmu.speed);\n if (overlapTime > danmu.time)\n return true;\n return false;\n });\n });\n return result && result[0] ? result[0].top : void 0;\n }\n // 静止弹幕没有重叠问题\n case 1:\n case 2:\n return void 0;\n }\n } else {\n switch (target.mode) {\n case 0:\n topMap.sort((prev, next) => {\n const nextMinRight = Math.min(...next.map((item) => item.right));\n const prevMinRight = Math.min(...prev.map((item) => item.right));\n return nextMinRight * next.length - prevMinRight * prev.length;\n });\n break;\n case 1:\n case 2:\n topMap.sort((prev, next) => {\n const nextMaxWidth = Math.max(...next.map((item) => item.width));\n const prevMaxWidth = Math.max(...prev.map((item) => item.width));\n return prevMaxWidth * prev.length - nextMaxWidth * next.length;\n });\n break;\n }\n return topMap[0][0].top;\n }\n}\nonmessage = (event) => {\n const { data } = event;\n if (!data.id || !data.type)\n return;\n const fns = { getDanmuTop };\n const fn = fns[data.type];\n const result = fn(data);\n globalThis.postMessage({\n result,\n id: data.id\n });\n};\n'; const blob = typeof self !== "undefined" && self.Blob && new Blob(["URL.revokeObjectURL(import.meta.url);", jsContent], { type: "text/javascript;charset=utf-8" }); function WorkerWrapper(options) { let objURL; try { objURL = blob && (self.URL || self.webkitURL).createObjectURL(blob); if (!objURL) throw ""; const worker = new Worker(objURL, { type: "module", name: options?.name }); worker.addEventListener("error", () => { (self.URL || self.webkitURL).revokeObjectURL(objURL); }); return worker; } catch (e) { return new Worker( "data:text/javascript;charset=utf-8," + encodeURIComponent(jsContent), { type: "module", name: options?.name } ); } } class Danmuku { constructor(art, option) { const { constructor, template } = art; this.utils = constructor.utils; this.validator = constructor.validator; this.$danmuku = template.$danmuku; this.$player = template.$player; this.art = art; this.queue = []; this.$refs = []; this.isStop = false; this.isHide = false; this.timer = null; this.index = 0; this.option = Danmuku.option; this.states = { wait: [], ready: [], emit: [], stop: [] }; this.config(option, true); this.worker = new WorkerWrapper(); this.start = this.start.bind(this); this.stop = this.stop.bind(this); this.reset = this.reset.bind(this); this.resize = this.resize.bind(this); this.destroy = this.destroy.bind(this); art.on("video:play", this.start); art.on("video:playing", this.start); art.on("video:pause", this.stop); art.on("video:waiting", this.stop); art.on("destroy", this.destroy); art.on("resize", this.resize); this.load(); } // 默认配置 static get option() { return { danmuku: [], // 弹幕数据 speed: 5, // 弹幕持续时间,范围在[1 ~ 10] margin: [10, "25%"], // 弹幕上下边距,支持像素数字和百分比 opacity: 1, // 弹幕透明度,范围在[0 ~ 1] color: "#FFFFFF", // 默认弹幕颜色,可以被单独弹幕项覆盖 mode: 0, // 默认弹幕模式: 0: 滚动,1: 顶部,2: 底部 modes: [0, 1, 2], // 弹幕可见的模式 fontSize: 25, // 弹幕字体大小,支持像素数字和百分比 antiOverlap: true, // 弹幕是否防重叠 synchronousPlayback: false, // 是否同步播放速度 mount: void 0, // 弹幕发射器挂载点, 默认为播放器控制栏中部 heatmap: false, // 是否开启热力图 width: 512, // 当播放器宽度小于此值时,弹幕发射器置于播放器底部 points: [], // 热力图数据 filter: () => true, // 弹幕载入前的过滤器,只支持返回布尔值 beforeEmit: () => true, // 弹幕发送前的过滤器,支持返回 Promise beforeVisible: () => true, // 弹幕显示前的过滤器,支持返回 Promise visible: true, // 弹幕层是否可见 emitter: true, // 是否开启弹幕发射器 maxLength: 200, // 弹幕输入框最大长度, 范围在[1 ~ 1000] lockTime: 5, // 输入框锁定时间,范围在[1 ~ 60] theme: "dark", // 弹幕主题,支持 dark 和 light,只在自定义挂载时生效 OPACITY: {}, // 不透明度配置项 FONT_SIZE: {}, // 弹幕字号配置项 MARGIN: {}, // 显示区域配置项 SPEED: {}, // 弹幕速度配置项 COLOR: [] // 颜色列表配置项 }; } // 配置校验 static get scheme() { return { danmuku: "array|function|string", speed: "number", margin: "array", opacity: "number", color: "string", mode: "number", modes: "array", fontSize: "number|string", antiOverlap: "boolean", synchronousPlayback: "boolean", mount: "?htmldivelement|string", heatmap: "object|boolean", width: "number", points: "array", filter: "function", beforeEmit: "function", beforeVisible: "function", visible: "boolean", emitter: "boolean", maxLength: "number", lockTime: "number", theme: "string", OPACITY: "object", FONT_SIZE: "object", MARGIN: "object", SPEED: "object", COLOR: "array" }; } // 初始弹幕样式 static get cssText() { return ` user-select: none; position: absolute; white-space: pre; pointer-events: none; perspective: 500px; display: inline-block; will-change: transform; font-weight: normal; line-height: 1.125; visibility: hidden; font-family: SimHei, "Microsoft JhengHei", Arial, Helvetica, sans-serif; text-shadow: rgb(0, 0, 0) 1px 0px 1px, rgb(0, 0, 0) 0px 1px 1px, rgb(0, 0, 0) 0px -1px 1px, rgb(0, 0, 0) -1px 0px 1px; `; } // 是否在移动端使用了自动旋屏,会影响弹幕的left和top值 get isRotate() { return this.art.plugins?.autoOrientation?.state; } // 计算上空白边距 get marginTop() { const { clamp } = this.utils; const value = this.option.margin[0]; const { clientHeight } = this.$player; if (typeof value === "number") { return clamp(value, 0, clientHeight); } if (typeof value === "string" && value.endsWith("%")) { const ratio = Number.parseFloat(value) / 100; return clamp(clientHeight * ratio, 0, clientHeight); } return Danmuku.option.margin[0]; } // 计算下空白边距 get marginBottom() { const { clamp } = this.utils; const value = this.option.margin[1]; const { clientHeight } = this.$player; if (typeof value === "number") { return clamp(value, 0, clientHeight); } if (typeof value === "string" && value.endsWith("%")) { const ratio = Number.parseFloat(value) / 100; return clamp(clientHeight * ratio, 0, clientHeight); } return Danmuku.option.margin[1]; } // 计算弹幕字体大小 get fontSize() { const { clamp } = this.utils; const { clientHeight } = this.$player; const fontSize = this.option.fontSize; if (typeof fontSize === "number") { return Math.round(clamp(fontSize, 12, clientHeight)); } if (typeof fontSize === "string" && fontSize.endsWith("%")) { const ratio = Number.parseFloat(fontSize) / 100; return Math.round(clamp(clientHeight * ratio, 12, clientHeight)); } return Danmuku.option.fontSize; } // 获取弹幕DOM节点 get $ref() { const $ref = this.$refs.pop() || document.createElement("div"); $ref.style.cssText = Danmuku.cssText; $ref.dataset.mode = ""; $ref.dataset.id = ""; $ref.className = ""; return $ref; } // 获取准备好发送的弹幕 get readys() { const { currentTime } = this.art; const result = []; this.filter("ready", (danmu) => result.push(danmu)); this.filter("wait", (danmu) => { if (currentTime + 0.1 >= danmu.time && danmu.time >= currentTime - 0.1) { result.push(danmu); } }); return result; } // 可见的弹幕的数据,用于计算下一个弹幕的top值 get visibles() { const result = []; const { clientWidth } = this.$player; const clientLeft = this.getLeft(this.$player); this.filter("emit", (danmu) => { const top = danmu.$ref.offsetTop; const left = this.getLeft(danmu.$ref) - clientLeft; const height = danmu.$ref.clientHeight; const width = danmu.$ref.clientWidth; const distance = left + width; const right = clientWidth - distance; const speed = distance / danmu.$restTime; const emit = {}; emit.top = top; emit.left = left; emit.height = height; emit.width = width; emit.right = right; emit.speed = speed; emit.distance = distance; emit.time = danmu.$restTime; emit.mode = danmu.mode; result.push(emit); }); return result; } // 计算弹幕速度 get speed() { return this.option.synchronousPlayback && this.art.playbackRate ? this.option.speed / Number(this.art.playbackRate) : this.option.speed; } // 加载弹幕 async load(danmuku) { const { errorHandle } = this.utils; let danmus = []; const target = danmuku || this.option.danmuku; try { if (typeof target === "function") { danmus = await target(); } else if (target instanceof Promise) { danmus = await target; } else if (typeof target === "string") { danmus = await bilibiliDanmuParseFromUrl(target); } else if (Array.isArray(target)) { danmus = target; } errorHandle(Array.isArray(danmus), "Danmuku need return an array as result"); if (danmuku === void 0) { this.reset(); this.queue = []; this.states = { wait: [], ready: [], emit: [], stop: [] }; this.$refs = []; this.$danmuku.textContent = ""; } for (let index = 0; index < danmus.length; index++) { const danmu = danmus[index]; await this.emit(danmu); } this.art.emit("artplayerPluginDanmuku:loaded", this.queue); } catch (error) { this.art.emit("artplayerPluginDanmuku:error", error); throw error; } return this; } // 把原始弹幕转换到弹幕队列 async emit(danmu) { const { clamp } = this.utils; this.validator(danmu, { id: "?string", // 弹幕唯一标识 text: "string", // 弹幕文本 mode: "?number", // 弹幕模式: 0: 滚动,1: 顶部,2: 底部 color: "?string", // 弹幕颜色 time: "?number", // 弹幕时间 border: "?boolean", // 弹幕是否有边框 style: "?object" // 弹幕额外样式 }); if (!danmu.text.trim()) return this; if (danmu.time) { danmu.time = clamp(danmu.time, 0, Infinity); } else { danmu.time = this.art.currentTime + 0.5; } if (danmu.mode === void 0) { danmu.mode = this.option.mode; } if (danmu.style === void 0) { danmu.style = {}; } if (danmu.color === void 0) { danmu.color = this.option.color; } if (![0, 1, 2].includes(danmu.mode)) return this; if (!this.option.filter(danmu)) return this; const item = { ...danmu, $state: "wait", // 弹幕初始状态 $index: this.index++, // 弹幕索引 $ref: null, // 弹幕 DOM 节点 $restTime: 0, // 弹幕剩余时间 $lastStartTime: 0 // 弹幕上次开始时间 }; this.setState(item, "wait"); this.queue.push(item); return this; } // 动态配置 config(option, isInit = false) { const { clamp } = this.utils; const { $controlsCenter } = this.art.template; const changed = Object.keys(option).some( (key) => JSON.stringify(this.option[key]) !== JSON.stringify(option[key]) ); if (!changed && !isInit) return this; this.option = Object.assign({}, Danmuku.option, this.option, option); this.validator(this.option, Danmuku.scheme); this.option.mode = clamp(this.option.mode, 0, 2); this.option.speed = clamp(this.option.speed, 1, 10); this.option.opacity = clamp(this.option.opacity, 0, 1); this.option.lockTime = clamp(this.option.lockTime, 1, 60); this.option.maxLength = clamp(this.option.maxLength, 1, 1e3); this.option.mount = this.option.mount || $controlsCenter; if (option.fontSize) { this.reset(); } if (this.option.visible) { this.show(); } else { this.hide(); } this.art.emit("artplayerPluginDanmuku:config", this.option); return this; } // 计算DOM的left值,受到旋屏影响 getLeft($ref) { const rect = $ref.getBoundingClientRect(); return this.isRotate ? rect.top : rect.left; } // 复杂运算交给 Web Worker 处理 postMessage(message = {}) { return new Promise((resolve) => { message.id = Date.now(); this.worker.postMessage(message); this.worker.onmessage = (event) => { const { data } = event; if (data.id === message.id) { resolve(data); } }; }); } // 根据状态获取弹幕 filter(state, callback) { const danmus = this.states[state] || []; for (let index = 0; index < danmus.length; index++) { callback(danmus[index]); } return danmus; } // 设置弹幕状态 setState(danmu, state) { this.states[danmu.$state] = this.states[danmu.$state].filter((item) => item !== danmu); danmu.$state = state; if (danmu.$ref) { danmu.$ref.dataset.state = state; } this.states[state].push(danmu); } // 重置弹幕到wait状态,回收弹幕DOM节点 makeWait(danmu) { this.setState(danmu, "wait"); if (danmu.$ref) { danmu.$ref.style.cssText = Danmuku.cssText; danmu.$ref.style.visibility = "hidden"; danmu.$ref.style.marginLeft = "0px"; danmu.$ref.style.transform = "translateX(0px)"; danmu.$ref.style.transition = "transform 0s linear 0s"; this.$refs.push(danmu.$ref); danmu.$ref = null; } } // 实时更新弹幕 update() { const { setStyles } = this.utils; this.timer = window.requestAnimationFrame(async () => { if (this.art.playing && !this.isHide) { this.filter("emit", (danmu) => { const emitTime = (Date.now() - danmu.$lastStartTime) / 1e3; danmu.$restTime -= emitTime; danmu.$lastStartTime = Date.now(); if (danmu.$restTime <= 0) { this.makeWait(danmu); } }); const readys = this.readys; for (let index = 0; index < readys.length; index++) { const danmu = readys[index]; const state = await this.option.beforeVisible(danmu); if (state) { const { clientWidth, clientHeight } = this.$player; danmu.$ref = this.$ref; danmu.$ref.textContent = danmu.text; this.$danmuku.appendChild(danmu.$ref); danmu.$ref.style.opacity = this.option.opacity; danmu.$ref.style.fontSize = `${this.fontSize}px`; danmu.$ref.style.color = danmu.color; danmu.$ref.style.border = danmu.border ? `1px solid ${danmu.color}` : null; danmu.$ref.style.backgroundColor = danmu.border ? "rgb(0 0 0 / 50%)" : null; setStyles(danmu.$ref, danmu.style); danmu.$lastStartTime = Date.now(); danmu.$restTime = this.speed; const distance = clientWidth + danmu.$ref.clientWidth; const { result: top } = await this.postMessage({ type: "getDanmuTop", target: { mode: danmu.mode, height: danmu.$ref.clientHeight, speed: distance / danmu.$restTime }, // 当前弹幕信息 visibles: this.visibles, // 可见的弹幕的数据 antiOverlap: this.option.antiOverlap, clientWidth, clientHeight, marginBottom: this.marginBottom, marginTop: this.marginTop }); if (danmu.$ref) { if (!this.isStop && top !== void 0) { this.setState(danmu, "emit"); danmu.$ref.style.top = `${top}px`; danmu.$ref.style.visibility = "visible"; danmu.$ref.dataset.mode = danmu.mode; danmu.$ref.dataset.id = danmu.id || ""; switch (danmu.mode) { // 滚动的弹幕 case 0: { danmu.$ref.style.left = `${clientWidth}px`; danmu.$ref.style.marginLeft = "0px"; danmu.$ref.style.transform = `translateX(${-distance}px)`; danmu.$ref.style.transition = `transform ${danmu.$restTime}s linear 0s`; break; } case 1: // falls through case 2: danmu.$ref.style.left = "50%"; danmu.$ref.style.marginLeft = `-${danmu.$ref.clientWidth / 2}px`; break; } this.art.emit("artplayerPluginDanmuku:visible", danmu); } else { this.setState(danmu, "ready"); this.$refs.push(danmu.$ref); danmu.$ref = null; } } } } } if (!this.isStop) { this.update(); } }); return this; } // 重置正在显示的弹幕: stop/emit 状态的弹幕 resize() { const { clientWidth } = this.$player; this.filter("stop", (danmu) => { switch (danmu.mode) { // 滚动的弹幕 case 0: danmu.$ref.style.left = `${clientWidth}px`; break; } }); this.filter("emit", (danmu) => { danmu.$lastStartTime = Date.now(); switch (danmu.mode) { // 滚动的弹幕 case 0: { const distance = clientWidth + danmu.$ref.clientWidth; danmu.$ref.style.left = `${clientWidth}px`; danmu.$ref.style.transform = `translateX(${-distance}px)`; danmu.$ref.style.transition = `transform ${danmu.$restTime}s linear 0s`; break; } } }); } // 继续弹幕 continue() { const { clientWidth } = this.$player; this.filter("stop", (danmu) => { this.setState(danmu, "emit"); danmu.$lastStartTime = Date.now(); switch (danmu.mode) { // 继续滚动的弹幕 case 0: { const distance = clientWidth + danmu.$ref.clientWidth; danmu.$ref.style.transform = `translateX(${-distance}px)`; danmu.$ref.style.transition = `transform ${danmu.$restTime}s linear 0s`; break; } } }); return this; } // 暂停弹幕 suspend() { const { clientWidth } = this.$player; this.filter("emit", (danmu) => { this.setState(danmu, "stop"); switch (danmu.mode) { // 停止滚动的弹幕 case 0: { const translateX = clientWidth - (this.getLeft(danmu.$ref) - this.getLeft(this.$player)); danmu.$ref.style.transform = `translateX(${-translateX}px)`; danmu.$ref.style.transition = "transform 0s linear 0s"; break; } } }); return this; } stop() { this.isStop = true; this.suspend(); window.cancelAnimationFrame(this.timer); this.art.emit("artplayerPluginDanmuku:stop"); return this; } start() { this.isStop = false; this.continue(); this.update(); this.art.emit("artplayerPluginDanmuku:start"); return this; } reset() { this.queue.forEach((danmu) => this.makeWait(danmu)); this.art.emit("artplayerPluginDanmuku:reset"); return this; } show() { this.isHide = false; this.$danmuku.style.opacity = 1; this.option.visible = true; this.art.emit("artplayerPluginDanmuku:show"); return this; } hide() { this.isHide = true; this.$danmuku.style.opacity = 0; this.option.visible = false; this.art.emit("artplayerPluginDanmuku:hide"); return this; } destroy() { this.stop(); this.worker.terminate(); this.art.off("video:play", this.start); this.art.off("video:playing", this.start); this.art.off("video:pause", this.stop); this.art.off("video:waiting", this.stop); this.art.off("resize", this.reset); this.art.off("destroy", this.destroy); this.art.emit("artplayerPluginDanmuku:destroy"); } } const lib = { map(value, inMin, inMax, outMin, outMax) { return (value - inMin) * (outMax - outMin) / (inMax - inMin) + outMin; }, range(start, end, tick) { const s = Math.round(start / tick) * tick; return Array.from( { length: Math.floor((end - start) / tick) }, (v, k) => { return k * tick + s; } ); } }; function line(pointA, pointB) { const lengthX = pointB[0] - pointA[0]; const lengthY = pointB[1] - pointA[1]; return { length: Math.sqrt(lengthX ** 2 + lengthY ** 2), angle: Math.atan2(lengthY, lengthX) }; } function heatmap(art, danmuku, option) { const { query } = art.constructor.utils; art.controls.add({ name: "heatmap", position: "top", html: "", style: { position: "absolute", top: "-100px", left: "0px", right: "0px", height: "100px", width: "100%", pointerEvents: "none" }, mounted($heatmap) { let $start = null; let $stop = null; function update(arg = []) { $start = null; $stop = null; $heatmap.innerHTML = ""; if (!art.duration || art.option.isLive) return; const svg = { w: $heatmap.offsetWidth, h: $heatmap.offsetHeight }; const options = { xMin: 0, xMax: svg.w, yMin: 0, yMax: 128, scale: 0.25, opacity: 0.2, minHeight: Math.floor(svg.h * 0.05), sampling: Math.floor(svg.w / 100), smoothing: 0.2, flattening: 0.2 }; if (typeof option === "object") { Object.assign(options, option); } let points = []; if (Array.isArray(arg) && arg.length) { points = [...arg]; } else { const gap = art.duration / svg.w; for (let x = 0; x <= svg.w; x += options.sampling) { const y = danmuku.queue.filter( ({ time }) => time > x * gap && time <= (x + options.sampling) * gap ).length; points.push([x, y]); } } if (points.length === 0) return; const lastPoint = points[points.length - 1]; const lastX = lastPoint[0]; const lastY = lastPoint[1]; if (lastX !== svg.w) { points.push([svg.w, lastY]); } const yPoints = points.map((point) => point[1]); const yMin = Math.min(...yPoints); const yMax = Math.max(...yPoints); const yMid = (yMin + yMax) / 2; for (let i = 0; i < points.length; i++) { const point = points[i]; const y = point[1]; point[1] = y * (y > yMid ? 1 + options.scale : 1 - options.scale) + options.minHeight; } const controlPoint = (current, previous, next, reverse) => { const p = previous || current; const n = next || current; const o = line(p, n); const flat = lib.map(Math.cos(o.angle) * options.flattening, 0, 1, 1, 0); const angle = o.angle * flat + (reverse ? Math.PI : 0); const length = o.length * options.smoothing; const x = current[0] + Math.cos(angle) * length; const y = current[1] + Math.sin(angle) * length; return [x, y]; }; const bezierCommand = (point, i, a) => { const cps = controlPoint(a[i - 1], a[i - 2], point); const cpe = controlPoint(point, a[i - 1], a[i + 1], true); const close = i === a.length - 1 ? " z" : ""; return `C ${cps[0]},${cps[1]} ${cpe[0]},${cpe[1]} ${point[0]},${point[1]}${close}`; }; const pointsPositions = points.map((e) => { const x = lib.map(e[0], options.xMin, options.xMax, 0, svg.w); const y = lib.map(e[1], options.yMin, options.yMax, svg.h, 0); return [x, y]; }); const pathD = pointsPositions.reduce( (acc, e, i, a) => i === 0 ? `M ${a[a.length - 1][0]},${svg.h} L ${e[0]},${svg.h} L ${e[0]},${e[1]}` : `${acc} ${bezierCommand(e, i, a)}`, "" ); $heatmap.innerHTML = ` `; $start = query("#heatmap-start", $heatmap); $stop = query("#heatmap-stop", $heatmap); $start.setAttribute("offset", `${art.played * 100}%`); $stop.setAttribute("offset", `${art.played * 100}%`); } art.on("video:timeupdate", () => { if ($start && $stop) { $start.setAttribute("offset", `${art.played * 100}%`); $stop.setAttribute("offset", `${art.played * 100}%`); } }); art.on("setBar", (type, percentage) => { if ($start && $stop && type === "played") { $start.setAttribute("offset", `${percentage * 100}%`); $stop.setAttribute("offset", `${percentage * 100}%`); } }); art.on("ready", () => update()); art.on("resize", () => update()); art.on("artplayerPluginDanmuku:loaded", () => update()); art.on("artplayerPluginDanmuku:points", (points) => update(points)); } }); } const $check_off = ''; const $check_on = ''; const $config = ''; const $mode_0_off = ''; const $mode_0_on = ''; const $mode_1_off = ''; const $mode_1_on = ''; const $mode_2_off = ''; const $mode_2_on = ''; const $off = ''; const $on = ''; const $style = ''; const style = ".artplayer-plugin-danmuku {\n display: flex;\n position: relative;\n z-index: 99;\n align-items: center;\n justify-content: center;\n font-size: 12px;\n height: 32px;\n width: 100%;\n color: #fff;\n font-weight: 300;\n flex-shrink: 0;\n gap: 10px;\n}\n.artplayer-plugin-danmuku .apd-icon {\n cursor: pointer;\n opacity: 0.75;\n transition: all 0.2s ease;\n fill: #fff;\n}\n.artplayer-plugin-danmuku .apd-icon:hover {\n opacity: 1;\n}\n.artplayer-plugin-danmuku .apd-config {\n display: flex;\n position: relative;\n}\n.artplayer-plugin-danmuku .apd-config .apd-config-panel {\n position: absolute;\n bottom: 24px;\n left: 0;\n width: 320px;\n padding: 10px;\n opacity: 0;\n pointer-events: none;\n}\n.artplayer-plugin-danmuku .apd-config .apd-config-panel .apd-config-panel-inner {\n width: 100%;\n border-radius: 3px;\n background-color: rgba(0, 0, 0, 0.85);\n padding: 10px;\n}\n.artplayer-plugin-danmuku .apd-config:hover .apd-config-panel {\n opacity: 100;\n pointer-events: all;\n}\n.artplayer-plugin-danmuku .apd-config-mode,\n.artplayer-plugin-danmuku .apd-config-slider,\n.artplayer-plugin-danmuku .apd-config-other,\n.artplayer-plugin-danmuku .apd-style-mode {\n margin-bottom: 15px;\n}\n.artplayer-plugin-danmuku .apd-modes {\n display: flex;\n align-items: center;\n margin-top: 5px;\n gap: 20px;\n}\n.artplayer-plugin-danmuku .apd-modes .apd-mode {\n cursor: pointer;\n text-align: center;\n}\n.artplayer-plugin-danmuku .apd-modes .apd-mode:hover {\n color: #00a1d6;\n}\n.artplayer-plugin-danmuku .apd-config-slider {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n.artplayer-plugin-danmuku .apd-config-slider .apd-value {\n width: 32px;\n text-align: right;\n}\n.artplayer-plugin-danmuku .apd-slider {\n position: relative;\n flex: 1;\n display: flex;\n height: 20px;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n}\n.artplayer-plugin-danmuku .apd-slider .apd-slider-line {\n position: relative;\n height: 2px;\n width: 100%;\n overflow: hidden;\n border-radius: 3px;\n background-color: rgba(255, 255, 255, 0.25);\n}\n.artplayer-plugin-danmuku .apd-slider .apd-slider-points {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: space-between;\n}\n.artplayer-plugin-danmuku .apd-slider .apd-slider-points .apd-slider-point {\n width: 2px;\n height: 2px;\n border-radius: 50%;\n background-color: rgba(255, 255, 255, 0.5);\n}\n.artplayer-plugin-danmuku .apd-slider .apd-slider-progress {\n width: 0%;\n height: 100%;\n background-color: #00a1d6;\n}\n.artplayer-plugin-danmuku .apd-slider .apd-slider-dot {\n position: absolute;\n transform: translateX(-6px);\n left: 0%;\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background-color: #00a1d6;\n}\n.artplayer-plugin-danmuku .apd-slider .apd-slider-steps {\n display: flex;\n align-items: center;\n justify-content: space-between;\n position: absolute;\n bottom: -12px;\n width: calc(100% + 32px);\n color: #777;\n}\n.artplayer-plugin-danmuku .apd-slider .apd-slider-steps .apd-slider-step {\n flex-shrink: 0;\n width: 36px;\n text-align: center;\n scale: 0.95;\n}\n.artplayer-plugin-danmuku .apd-config-other {\n display: flex;\n align-items: center;\n gap: 20px;\n}\n.artplayer-plugin-danmuku .apd-config-other .apd-check-off,\n.artplayer-plugin-danmuku .apd-config-other .apd-check-on {\n width: 16px;\n height: 16px;\n}\n.artplayer-plugin-danmuku .apd-config-other .apd-other {\n display: flex;\n align-items: center;\n cursor: pointer;\n gap: 2px;\n}\n.artplayer-plugin-danmuku .apd-config-other .apd-other:hover {\n color: #00a1d6;\n}\n.artplayer-plugin-danmuku .apd-emitter {\n display: flex;\n flex: 1;\n align-items: center;\n height: 100%;\n background-color: rgba(255, 255, 255, 0.25);\n border-radius: 5px;\n}\n.artplayer-plugin-danmuku .apd-style {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.artplayer-plugin-danmuku .apd-style .apd-style-panel {\n position: absolute;\n bottom: 24px;\n left: 0;\n width: 200px;\n padding: 10px;\n opacity: 0;\n pointer-events: none;\n}\n.artplayer-plugin-danmuku .apd-style .apd-style-panel .apd-style-panel-inner {\n width: 100%;\n border-radius: 3px;\n background-color: rgba(0, 0, 0, 0.85);\n padding: 10px;\n}\n.artplayer-plugin-danmuku .apd-style:hover .apd-style-panel {\n opacity: 100;\n pointer-events: all;\n}\n.artplayer-plugin-danmuku .apd-colors {\n display: flex;\n flex-wrap: wrap;\n margin-top: 5px;\n gap: 8px;\n}\n.artplayer-plugin-danmuku .apd-colors .apd-color {\n width: 16px;\n height: 16px;\n border-radius: 2px;\n cursor: pointer;\n}\n.artplayer-plugin-danmuku .apd-colors .apd-color.apd-active {\n border: 1px solid black;\n box-shadow: 0 0 0 1px #fff;\n}\n.artplayer-plugin-danmuku .apd-input {\n outline: none;\n height: 100%;\n flex: 1;\n min-width: 0;\n width: auto;\n border: none;\n line-height: 1;\n color: #fff;\n background-color: transparent;\n}\n.artplayer-plugin-danmuku .apd-input::placeholder {\n color: rgba(255, 255, 255, 0.5);\n}\n.artplayer-plugin-danmuku .apd-send {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n width: 60px;\n flex-shrink: 0;\n cursor: pointer;\n text-shadow: none;\n border-top-right-radius: 5px;\n border-bottom-right-radius: 5px;\n background-color: #00a1d6;\n}\n.artplayer-plugin-danmuku .apd-send.apd-lock {\n cursor: not-allowed;\n color: #666;\n background-color: #e7e7e7;\n}\n.art-controls-center .apd-emitter {\n width: 260px;\n flex: none;\n}\n.art-fullscreen .artplayer-plugin-danmuku,\n.art-fullscreen-web .artplayer-plugin-danmuku {\n height: 38px;\n gap: 16px;\n}\n.art-fullscreen .artplayer-plugin-danmuku .apd-config-icon,\n.art-fullscreen-web .artplayer-plugin-danmuku .apd-config-icon,\n.art-fullscreen .artplayer-plugin-danmuku .apd-toggle-off,\n.art-fullscreen-web .artplayer-plugin-danmuku .apd-toggle-off,\n.art-fullscreen .artplayer-plugin-danmuku .apd-toggle-on,\n.art-fullscreen-web .artplayer-plugin-danmuku .apd-toggle-on {\n width: 28px;\n height: 28px;\n}\n.art-fullscreen .artplayer-plugin-danmuku .apd-emitter,\n.art-fullscreen-web .artplayer-plugin-danmuku .apd-emitter {\n width: 400px;\n flex: none;\n}\n.art-video-player > .artplayer-plugin-danmuku {\n position: absolute;\n left: 0;\n right: 0;\n bottom: -40px;\n padding: 0 10px;\n}\n.art-video-player:has(> .artplayer-plugin-danmuku) {\n margin-bottom: 40px;\n}\n[data-danmuku-emitter='false'] .apd-emitter {\n display: none !important;\n}\n[data-danmuku-emitter='false'] .art-controls-center .artplayer-plugin-danmuku {\n justify-content: flex-end;\n gap: 18px;\n}\n[data-danmuku-emitter='false'].art-fullscreen .art-controls-center .artplayer-plugin-danmuku,\n[data-danmuku-emitter='false'].art-fullscreen-web .art-controls-center .artplayer-plugin-danmuku {\n gap: 24px;\n}\n[data-danmuku-theme='light'] > .artplayer-plugin-danmuku .apd-icon {\n fill: #333;\n}\n[data-danmuku-theme='light'] > .artplayer-plugin-danmuku .apd-emitter {\n background-color: #f1f2f3;\n}\n[data-danmuku-theme='light'] > .artplayer-plugin-danmuku .apd-input {\n color: #000;\n}\n[data-danmuku-theme='light'] > .artplayer-plugin-danmuku .apd-input::placeholder {\n color: rgba(0, 0, 0, 0.3);\n}\n[data-danmuku-visible='false'] .apd-toggle-off {\n display: block;\n}\n[data-danmuku-visible='false'] .apd-toggle-on {\n display: none;\n}\n[data-danmuku-visible='true'] .apd-toggle-off {\n display: none;\n}\n[data-danmuku-visible='true'] .apd-toggle-on {\n display: block;\n}\n[data-danmuku-anti-overlap='false'] .apd-anti-overlap .apd-check-on {\n display: none;\n}\n[data-danmuku-anti-overlap='false'] .apd-anti-overlap .apd-check-off {\n display: block;\n}\n[data-danmuku-anti-overlap='true'] .apd-anti-overlap .apd-check-on {\n display: block;\n}\n[data-danmuku-anti-overlap='true'] .apd-anti-overlap .apd-check-off {\n display: none;\n}\n[data-danmuku-sync-video='false'] .apd-sync-video .apd-check-on {\n display: none;\n}\n[data-danmuku-sync-video='false'] .apd-sync-video .apd-check-off {\n display: block;\n}\n[data-danmuku-sync-video='true'] .apd-sync-video .apd-check-on {\n display: block;\n}\n[data-danmuku-sync-video='true'] .apd-sync-video .apd-check-off {\n display: none;\n}\n[data-danmuku-mode0='false'] .apd-config-mode .apd-mode-0-off {\n display: block;\n}\n[data-danmuku-mode0='false'] .apd-config-mode .apd-mode-0-on {\n display: none;\n}\n[data-danmuku-mode0='false'] .art-danmuku [data-mode='0'] {\n opacity: 0 !important;\n}\n[data-danmuku-mode0='true'] .apd-config-mode .apd-mode-0-off {\n display: none;\n}\n[data-danmuku-mode0='true'] .apd-config-mode .apd-mode-0-on {\n display: block;\n}\n[data-danmuku-mode='0'] .apd-style-mode [data-mode='0'] {\n color: #00a1d6;\n}\n[data-danmuku-mode='0'] .apd-style-mode [data-mode='0'] path {\n fill: #00a1d6;\n}\n[data-danmuku-mode1='false'] .apd-config-mode .apd-mode-1-off {\n display: block;\n}\n[data-danmuku-mode1='false'] .apd-config-mode .apd-mode-1-on {\n display: none;\n}\n[data-danmuku-mode1='false'] .art-danmuku [data-mode='1'] {\n opacity: 0 !important;\n}\n[data-danmuku-mode1='true'] .apd-config-mode .apd-mode-1-off {\n display: none;\n}\n[data-danmuku-mode1='true'] .apd-config-mode .apd-mode-1-on {\n display: block;\n}\n[data-danmuku-mode='1'] .apd-style-mode [data-mode='1'] {\n color: #00a1d6;\n}\n[data-danmuku-mode='1'] .apd-style-mode [data-mode='1'] path {\n fill: #00a1d6;\n}\n[data-danmuku-mode2='false'] .apd-config-mode .apd-mode-2-off {\n display: block;\n}\n[data-danmuku-mode2='false'] .apd-config-mode .apd-mode-2-on {\n display: none;\n}\n[data-danmuku-mode2='false'] .art-danmuku [data-mode='2'] {\n opacity: 0 !important;\n}\n[data-danmuku-mode2='true'] .apd-config-mode .apd-mode-2-off {\n display: none;\n}\n[data-danmuku-mode2='true'] .apd-config-mode .apd-mode-2-on {\n display: block;\n}\n[data-danmuku-mode='2'] .apd-style-mode [data-mode='2'] {\n color: #00a1d6;\n}\n[data-danmuku-mode='2'] .apd-style-mode [data-mode='2'] path {\n fill: #00a1d6;\n}\n"; class Setting { constructor(art, danmuku) { this.art = art; this.danmuku = danmuku; this.utils = art.constructor.utils; const { setStyle } = this.utils; const { $controlsCenter } = art.template; setStyle($controlsCenter, "display", "flex"); this.template = { $controlsCenter, $mount: $controlsCenter, $danmuku: null, $toggle: null, $config: null, $configPanel: null, $configModes: null, $style: null, $stylePanel: null, $styleModes: null, $colors: null, $opacitySlider: null, $opacityValue: null, $marginSlider: null, $marginValue: null, $fontSizeSlider: null, $fontSizeValue: null, $speedSlider: null, $speedValue: null, $input: null, $send: null }; this.slider = { opacity: null, margin: null, fontSize: null, speed: null }; this.emitting = false; this.isLock = false; this.timer = null; this.createTemplate(); this.createSliders(); this.createEvents(); this.mount(this.option.mount); art.on("resize", () => this.resize()); art.on("fullscreen", (state) => this.onFullscreen(state)); art.on("fullscreenWeb", (state) => this.onFullscreen(state)); art.proxy(this.template.$config, "mouseenter", () => { this.onMouseEnter({ $control: this.template.$config, $panel: this.template.$configPanel }); }); art.proxy(this.template.$style, "mouseenter", () => { this.onMouseEnter({ $control: this.template.$style, $panel: this.template.$stylePanel }); }); } static get icons() { return { $on, $off, $config, $style, $mode_0_off, $mode_0_on, $mode_1_off, $mode_1_on, $mode_2_off, $mode_2_on, $check_on, $check_off }; } get option() { return this.danmuku.option; } get outside() { return this.template.$mount !== this.template.$controlsCenter; } get TEMPLATE() { const { option } = this; return `

${$on}${$off}
${$config}
按类型屏蔽
${$mode_0_off}${$mode_0_on}
滚动
${$mode_1_off}${$mode_1_on}
顶部
${$mode_2_off}${$mode_2_on}
底部
${$check_on}${$check_off} 防止弹幕重叠
${$check_on}${$check_off} 同步视频速度
不透明度
未知
显示区域
未知
弹幕字号
未知
弹幕速度
未知
${$style}
模式
${$mode_0_on}
滚动
${$mode_1_on}
顶部
${$mode_2_on}
底部
颜色
${this.COLOR.map((color) => `
`).join("")}
发送
`; } get OPACITY() { return { min: 0, max: 100, steps: [], ...this.option.OPACITY }; } get FONT_SIZE() { return { min: 12, max: 120, steps: [], ...this.option.FONT_SIZE }; } get MARGIN() { return { min: 0, max: 3, steps: [ { name: "1/4", value: [10, "75%"] }, { name: "半屏", value: [10, "50%"] }, { name: "3/4", value: [10, "25%"] }, { name: "满屏", value: [10, 10] } ], ...this.option.MARGIN }; } get SPEED() { return { min: 0, max: 4, steps: [ { name: "极慢", value: 10 }, { name: "较慢", value: 7.5, hide: true }, { name: "适中", value: 5 }, { name: "较快", value: 2.5, hide: true }, { name: "极快", value: 1 } ], ...this.option.SPEED }; } get COLOR() { return this.option.COLOR.length ? this.option.COLOR : [ "#FE0302", "#FF7204", "#FFAA02", "#FFD302", "#FFFF00", "#A0EE00", "#00CD00", "#019899", "#4266BE", "#89D5FF", "#CC0273", "#222222", "#9B9B9B", "#FFFFFF" ]; } query(selector) { const { query } = this.utils; const { $danmuku } = this.template; return query(selector, $danmuku); } append(el, target) { const { append } = this.utils; const children = [...el.children]; if (children.includes(target)) return; append(el, target); } setData(key, value) { const { $player } = this.art.template; const { $mount } = this.template; $player.dataset[key] = value; if (this.outside) { $mount.dataset[key] = value; } } createTemplate() { const { createElement, tooltip } = this.utils; const $danmuku = createElement("div"); $danmuku.className = "artplayer-plugin-danmuku"; $danmuku.innerHTML = this.TEMPLATE; this.template.$danmuku = $danmuku; this.template.$toggle = this.query(".apd-toggle"); this.template.$config = this.query(".apd-config"); this.template.$configPanel = this.query(".apd-config-panel"); this.template.$configModes = this.query(".apd-config-mode .apd-modes"); this.template.$style = this.query(".apd-style"); this.template.$stylePanel = this.query(".apd-style-panel"); this.template.$styleModes = this.query(".apd-style-mode .apd-modes"); this.template.$colors = this.query(".apd-colors"); this.template.$antiOverlap = this.query(".apd-anti-overlap"); this.template.$syncVideo = this.query(".apd-sync-video"); this.template.$opacitySlider = this.query(".apd-config-opacity .apd-slider"); this.template.$opacityValue = this.query(".apd-config-opacity .apd-value"); this.template.$marginSlider = this.query(".apd-config-margin .apd-slider"); this.template.$marginValue = this.query(".apd-config-margin .apd-value"); this.template.$fontSizeSlider = this.query(".apd-config-fontSize .apd-slider"); this.template.$fontSizeValue = this.query(".apd-config-fontSize .apd-value"); this.template.$speedSlider = this.query(".apd-config-speed .apd-slider"); this.template.$speedValue = this.query(".apd-config-speed .apd-value"); this.template.$input = this.query(".apd-input"); this.template.$send = this.query(".apd-send"); const { $toggle } = this.template; this.art.on("artplayerPluginDanmuku:show", () => { tooltip($toggle, "关闭弹幕"); }); this.art.on("artplayerPluginDanmuku:hide", () => { tooltip($toggle, "打开弹幕"); }); } createEvents() { const { $toggle, $configModes, $styleModes, $colors, $antiOverlap, $syncVideo, $send, $input } = this.template; this.art.proxy($toggle, "click", () => { this.danmuku.config({ visible: !this.option.visible }); this.reset(); }); this.art.proxy($configModes, "click", (event) => { const $mode = event.target.closest(".apd-mode"); if (!$mode) return; const mode = Number($mode.dataset.mode); if (this.option.modes.includes(mode)) { this.danmuku.config({ modes: this.option.modes.filter((m) => m !== mode) }); } else { this.danmuku.config({ modes: [...this.option.modes, mode] }); } this.reset(); }); this.art.proxy($antiOverlap, "click", () => { this.danmuku.config({ antiOverlap: !this.option.antiOverlap }); this.reset(); }); this.art.proxy($syncVideo, "click", () => { this.danmuku.config({ synchronousPlayback: !this.option.synchronousPlayback }); this.reset(); }); this.art.proxy($styleModes, "click", (event) => { const $mode = event.target.closest(".apd-mode"); if (!$mode) return; const mode = Number($mode.dataset.mode); this.danmuku.config({ mode }); this.reset(); }); this.art.proxy($colors, "click", (event) => { const $color = event.target.closest(".apd-color"); if (!$color) return; this.danmuku.config({ color: $color.dataset.color }); this.reset(); }); this.art.proxy($send, "click", () => this.emit()); this.art.proxy($input, "keypress", (event) => { if (event.key === "Enter") { event.preventDefault(); this.emit(); } }); } createSliders() { this.slider.opacity = this.createSlider({ ...this.OPACITY, container: this.template.$opacitySlider, findIndex: () => { return Math.round(this.option.opacity * 100); }, onChange: (index) => { const { $opacityValue } = this.template; $opacityValue.textContent = `${index}%`; this.danmuku.config({ opacity: index / 100 }); } }); this.slider.margin = this.createSlider({ ...this.MARGIN, container: this.template.$marginSlider, findIndex: () => { return this.MARGIN.steps.findIndex( (item) => item.value[0] === this.option.margin[0] && item.value[1] === this.option.margin[1] ); }, onChange: (index) => { const margin = this.MARGIN.steps[index]; if (!margin) return; const { $marginValue } = this.template; $marginValue.textContent = margin.name; this.danmuku.config({ margin: margin.value }); } }); this.slider.fontSize = this.createSlider({ ...this.FONT_SIZE, container: this.template.$fontSizeSlider, findIndex: () => { return this.danmuku.fontSize; }, onChange: (index) => { const { $fontSizeValue } = this.template; $fontSizeValue.textContent = `${index}px`; if (index === this.danmuku.fontSize) return; this.danmuku.config({ fontSize: index }); } }); this.slider.speed = this.createSlider({ ...this.SPEED, container: this.template.$speedSlider, findIndex: () => { return this.SPEED.steps.findIndex((item) => item.value === this.option.speed); }, onChange: (index) => { const speed = this.SPEED.steps[index]; if (!speed) return; const { $speedValue } = this.template; $speedValue.textContent = speed.name; this.danmuku.config({ speed: speed.value }); } }); } createSlider({ min, max, container, findIndex, onChange, steps = [] }) { const { query, clamp, setStyle } = this.utils; setStyle(container, "touch-action", "none"); container.innerHTML = `
${steps.map(() => `
`).join("")}
${steps.map((step) => step.hide ? "" : `
${step.name}
`).join("")}
`; const $dot = query(".apd-slider-dot", container); const $progress = query(".apd-slider-progress", container); let isDroging = false; function reset(index = findIndex()) { if (index < min || index > max) return; const percentage = (index - min) / (max - min); $dot.style.left = `${percentage * 100}%`; if (steps.length === 0) { $progress.style.width = $dot.style.left; } onChange(index); } function updateLeft(event) { const { top, height, left, width } = container.getBoundingClientRect(); if (this.art.isRotate) { const value = clamp(event.clientY - top, 0, height); const index = Math.round(value / height * (max - min) + min); reset(index); } else { const value = clamp(event.clientX - left, 0, width); const index = Math.round(value / width * (max - min) + min); reset(index); } } this.art.proxy(container, "click", (event) => { updateLeft.call(this, event); }); this.art.proxy(container, "pointerdown", (event) => { isDroging = event.button === 0; }); this.art.on("document:pointermove", (event) => { if (isDroging) { updateLeft.call(this, event); } }); this.art.on("document:pointerup", (event) => { if (isDroging) { isDroging = false; updateLeft.call(this, event); } }); return { reset }; } onFullscreen(state) { const { $danmuku, $controlsCenter, $mount } = this.template; if (this.outside) { if (state) { this.append($controlsCenter, $danmuku); } else { this.append($mount, $danmuku); } } else { this.append($controlsCenter, $danmuku); } } onMouseEnter({ $control, $panel }) { const { $player } = this.art.template; const controlRect = $control.getBoundingClientRect(); const panelRect = $panel.getBoundingClientRect(); const playerRect = $player.getBoundingClientRect(); const half = panelRect.width / 2 - controlRect.width / 2; const left = playerRect.left - (controlRect.left - half); const right = controlRect.right + half - playerRect.right; if (left > 0) { $panel.style.left = `${-half + left}px`; } else if (right > 0) { $panel.style.left = `${-half - right}px`; } else { $panel.style.left = `${-half}px`; } } async emit() { const { $input } = this.template; const text = $input.value.trim(); if (!text.length) return; if (this.isLock) return; if (this.emitting) return; const danmu = { text, mode: this.option.mode, color: this.option.color, time: this.art.currentTime }; try { this.emitting = true; const state = await this.option.beforeEmit(danmu); this.emitting = false; if (state !== true) return; danmu.border = true; delete danmu.time; this.danmuku.emit(danmu); $input.value = ""; this.lock(); } catch (error) { console.error("Error emitting danmuku:", error); this.emitting = false; } } lock() { const { addClass } = this.utils; const { $send } = this.template; this.isLock = true; let time = this.option.lockTime; $send.textContent = time; addClass($send, "apd-lock"); const loop = () => { this.timer = setTimeout(() => { if (time === 0) { this.unlock(); } else { time -= 1; $send.textContent = time; loop(); } }, 1e3); }; loop(); } unlock() { const { removeClass } = this.utils; const { $send } = this.template; clearTimeout(this.timer); this.isLock = false; $send.textContent = "发送"; removeClass($send, "apd-lock"); } resize() { if (this.outside) return; if (this.art.fullscreen) return; if (this.art.fullscreenWeb) return; const { $player, $controlsCenter } = this.art.template; const { $danmuku } = this.template; if (this.art.width < this.option.width) { this.append($player, $danmuku); } else { this.append($controlsCenter, $danmuku); } } reset() { const { inverseClass, tooltip } = this.utils; const { $toggle, $colors } = this.template; this.slider.opacity.reset(); this.slider.margin.reset(); this.slider.fontSize.reset(); this.slider.speed.reset(); this.setData("danmukuVisible", this.option.visible); this.setData("danmukuMode", this.option.mode); this.setData("danmukuColor", this.option.color); this.setData("danmukuMode0", this.option.modes.includes(0)); this.setData("danmukuMode1", this.option.modes.includes(1)); this.setData("danmukuMode2", this.option.modes.includes(2)); this.setData("danmukuAntiOverlap", this.option.antiOverlap); this.setData("danmukuSyncVideo", this.option.synchronousPlayback); this.setData("danmukuTheme", this.option.theme); this.setData("danmukuEmitter", this.option.emitter); const colors = $colors.children; const $color = Array.from(colors).find((item) => item.dataset.color === this.option.color.toUpperCase()); $color && inverseClass($color, "apd-active"); tooltip($toggle, this.option.visible ? "关闭弹幕" : "打开弹幕"); this.resize(); } mount(target) { const { errorHandle } = this.utils; const $el = typeof target === "string" ? document.querySelector(target) : target; errorHandle($el, `Can not find the mount point: ${target}`); this.append($el, this.template.$danmuku); this.template.$mount = $el; this.reset(); } } if (typeof document !== "undefined") { const id = "artplayer-plugin-danmuku"; let $style2 = document.getElementById(id); if (!$style2) { $style2 = document.createElement("style"); $style2.id = id; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => { document.head.appendChild($style2); }); } else { (document.head || document.documentElement).appendChild($style2); } } $style2.textContent = style; } function artplayerPluginDanmuku(option) { return (art) => { const danmuku = new Danmuku(art, option); const setting = new Setting(art, danmuku); if (danmuku.option.heatmap) { heatmap(art, danmuku, danmuku.option.heatmap); } return { name: "artplayerPluginDanmuku", emit: danmuku.emit.bind(danmuku), load: danmuku.load.bind(danmuku), config: danmuku.config.bind(danmuku), hide: danmuku.hide.bind(danmuku), show: danmuku.show.bind(danmuku), reset: danmuku.reset.bind(danmuku), mount: setting.mount.bind(setting), get option() { return danmuku.option; }, get isHide() { return danmuku.isHide; }, get isStop() { return danmuku.isStop; } }; }; } artplayerPluginDanmuku.icons = Setting.icons; export { artplayerPluginDanmuku as default };