/*! * artplayer-plugin-danmuku.js v6.0.0 * Github: https://github.com/zhw2590582/ArtPlayer * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ function defaultOption() { return { danmuku: [], speed: 5, margin: [10, "25%"], opacity: 1, color: "#FFFFFF", mode: 0, modes: [0, 1, 2], fontSize: 25, antiOverlap: true, synchronousPlayback: false, mount: void 0, heatmap: false, width: 512, points: [], filter: () => true, beforeEmit: () => true, beforeVisible: () => true, visible: true, emitter: true, maxLength: 200, lockTime: 5, theme: "dark", OPACITY: {}, FONT_SIZE: {}, MARGIN: {}, SPEED: {}, COLOR: [] }; } function optionScheme() { return { danmuku: "array|function|string|promise", 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" }; } function isPromiseInput(value) { return value instanceof Promise || Object.prototype.toString.call(value) === "[object Promise]"; } function optionChanged(previous, update) { return Object.keys(update).some((key) => { const before = previous[key]; const after = update[key]; if (typeof before === "function" || typeof after === "function" || isPromiseInput(before) || isPromiseInput(after)) return before !== after; return JSON.stringify(before) !== JSON.stringify(after); }); } function normalizeOption(previous, update, { defaults, validate, clamp, mount }) { const next = Object.assign({}, defaults, previous, update); validate(next); next.mode = clamp(next.mode, 0, 2); next.speed = clamp(next.speed, 1, 10); next.opacity = clamp(next.opacity, 0, 1); next.lockTime = clamp(next.lockTime, 1, 60); next.maxLength = clamp(next.maxLength, 1, 1e3); next.mount = next.mount || mount; return next; } 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; const danmus = bilibiliDanmuParseFromXml(xml); globalThis.postMessage({ danmus, id }); } let nextRequest$1 = 0; function abortError(signal) { if (signal?.reason !== void 0) return signal.reason; const error = new Error("Bilibili Danmu loading cancelled"); error.name = "AbortError"; return error; } function bilibiliDanmuParseFromUrl(url, { signal, onCancel } = {}) { return new Promise((resolve, reject) => { let settled = false; let worker; let workerUrl; let releaseCancel; const id = ++nextRequest$1; const cancel = () => finish(void 0, abortError(signal)); function releaseWorker() { let error; let failed = false; try { if (worker) { const current = worker; worker = void 0; current.onmessage = null; current.onerror = null; current.onmessageerror = null; current.terminate(); } } catch (failure) { error = failure; failed = true; } if (workerUrl !== void 0) { const current = workerUrl; workerUrl = void 0; try { URL.revokeObjectURL(current); } catch (failure) { if (!failed) error = failure; failed = true; } } if (failed) throw error; } function finish(value, error) { if (settled) return; settled = true; let failed = arguments.length > 1; signal?.removeEventListener("abort", cancel); releaseCancel?.(); try { releaseWorker(); } catch (cleanupError) { if (!failed) error = cleanupError; failed = true; } if (failed) reject(error); else resolve(value); } function fallback(xml, error) { if (settled) return; console.error("Error parsing Bilibili Danmu:", error); try { finish(bilibiliDanmuParseFromXml(xml)); } catch (failure) { finish(void 0, failure); } } async function read() { const response = signal ? await fetch(url, { signal }) : await fetch(url); if (settled) return; const xml = await response.text(); if (settled) return; try { const workerText = ` ${getMode.toString()} ${bilibiliDanmuParseFromXml.toString()} onmessage = ${onmessage.toString()} `; workerUrl = URL.createObjectURL(new Blob([workerText], { type: "application/javascript" })); if (settled) { releaseWorker(); return; } worker = new Worker(workerUrl); if (settled) { releaseWorker(); return; } worker.onmessage = (event) => { if (settled || event.data?.id !== id) return; const { danmus } = event.data; if (!Array.isArray(danmus)) { finish(void 0, new Error("Invalid Bilibili Danmu worker response")); return; } finish(danmus); }; worker.onerror = (event) => { event.preventDefault?.(); fallback(xml, event.error || event); }; worker.onmessageerror = (event) => fallback(xml, event.error || event); worker.postMessage({ xml, id }); } catch (error) { fallback(xml, error); } } if (signal?.aborted) { cancel(); return; } signal?.addEventListener("abort", cancel); releaseCancel = onCancel?.(cancel); if (settled) { releaseCancel?.(); return; } read().catch((error) => finish(void 0, error)); }); } const inputs = /* @__PURE__ */ new WeakMap(); const cancelled$1 = /* @__PURE__ */ Symbol("cancelled danmuku input"); function stateFor(owner) { if (!inputs.has(owner)) inputs.set(owner, { closed: false, tasks: /* @__PURE__ */ new Set(), emitting: [] }); return inputs.get(owner); } function inputActive(owner) { const state = stateFor(owner); const task = state.emitting[state.emitting.length - 1]; return !state.closed && !owner.art.isDestroy && (!task || task.active()); } function beginInput(owner, replace) { const state = stateFor(owner); if (replace) { for (const task2 of [...state.tasks]) { if (task2.replace) task2.cancel(); } } let stopped = state.closed || owner.art.isDestroy; let resolveCancel; const cancellation = new Promise((resolve) => resolveCancel = resolve); const handlers = /* @__PURE__ */ new Set(); const controller = typeof AbortController === "function" ? new AbortController() : void 0; const task = { replace, signal: controller?.signal, active: () => !stopped && !state.closed && !owner.art.isDestroy, onCancel(callback) { if (!task.active()) { callback(); return () => { }; } handlers.add(callback); return () => handlers.delete(callback); }, cancel() { if (stopped) return; stopped = true; resolveCancel(cancelled$1); controller?.abort(); for (const callback of [...handlers]) callback(); handlers.clear(); }, wait: (value) => Promise.race([value, cancellation]), emit(danmu) { state.emitting.push(task); try { return owner.emit(danmu); } finally { state.emitting.pop(); } }, finish() { state.tasks.delete(task); handlers.clear(); } }; if (stopped) resolveCancel(cancelled$1); else state.tasks.add(task); return task; } function cancelInputs(owner) { const state = stateFor(owner); state.closed = true; for (const task of [...state.tasks]) task.cancel(); } function readInput(target, task) { if (typeof target === "function") return { asynchronous: true, value: target() }; if (isPromiseInput(target)) return { asynchronous: true, value: target }; if (typeof target === "string") return { asynchronous: true, value: bilibiliDanmuParseFromUrl(target, task) }; return { asynchronous: false, value: Array.isArray(target) ? target : [] }; } function filterState(owner, state, callback) { const danmus = owner.states[state] || []; for (let index = 0; index < danmus.length; index++) callback(danmus[index]); return danmus; } function readyItems(owner) { const { currentTime } = owner.art; const result = []; filterState(owner, "ready", (danmu) => result.push(danmu)); filterState(owner, "wait", (danmu) => { if (currentTime + 0.1 >= danmu.time && danmu.time >= currentTime - 0.1) result.push(danmu); }); return result; } function setItemState(owner, danmu, state) { owner.states[danmu.$state] = owner.states[danmu.$state].filter((item) => item !== danmu); danmu.$state = state; if (danmu.$ref) danmu.$ref.dataset.state = state; owner.states[state].push(danmu); } class Renderer { constructor(owner) { this.owner = owner; this.nodes = /* @__PURE__ */ new Set(); } acquire() { const $ref = this.owner.$refs.pop() || document.createElement("div"); $ref.style.cssText = this.owner.constructor.cssText; $ref.dataset.mode = ""; $ref.dataset.id = ""; $ref.className = ""; this.nodes.add($ref); return $ref; } get visibles() { const result = []; const { clientWidth } = this.owner.$player; const clientLeft = this.owner.getLeft(this.owner.$player); this.owner.filter("emit", (danmu) => { const top = danmu.$ref.offsetTop; const left = this.owner.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; } left($ref) { const rect = $ref.getBoundingClientRect(); return this.owner.isRotate ? rect.top : rect.left; } makeWait(danmu) { this.owner.setState(danmu, "wait"); if (danmu.$ref) { danmu.$ref.style.cssText = this.owner.constructor.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.owner.$refs.push(danmu.$ref); danmu.$ref = null; } } resize() { const { clientWidth } = this.owner.$player; this.owner.filter("stop", (danmu) => { switch (danmu.mode) { // 滚动的弹幕 case 0: danmu.$ref.style.left = `${clientWidth}px`; break; } }); this.owner.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.owner.$player; this.owner.filter("stop", (danmu) => { this.owner.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.owner.$player; this.owner.filter("emit", (danmu) => { this.owner.setState(danmu, "stop"); switch (danmu.mode) { // 停止滚动的弹幕 case 0: { const translateX = clientWidth - (this.owner.getLeft(danmu.$ref) - this.owner.getLeft(this.owner.$player)); danmu.$ref.style.transform = `translateX(${-translateX}px)`; danmu.$ref.style.transition = "transform 0s linear 0s"; break; } } }); return this; } prepare(danmu, operation) { const owner = this.owner; const ref = owner.$ref; operation.danmu = danmu; operation.ref = ref; danmu.$ref = ref; ref.textContent = danmu.text; owner.$danmuku.appendChild(ref); ref.style.opacity = owner.option.opacity; ref.style.fontSize = `${owner.fontSize}px`; ref.style.color = danmu.color; ref.style.border = danmu.border ? `1px solid ${danmu.color}` : null; ref.style.backgroundColor = danmu.border ? "rgb(0 0 0 / 50%)" : null; owner.utils.setStyles(ref, danmu.style); return ref; } place(danmu, ref, top, distance, clientWidth) { ref.style.top = `${top}px`; ref.style.visibility = "visible"; ref.dataset.mode = danmu.mode; ref.dataset.id = danmu.id || ""; switch (danmu.mode) { case 0: ref.style.left = `${clientWidth}px`; ref.style.marginLeft = "0px"; ref.style.transform = `translateX(${-distance}px)`; ref.style.transition = `transform ${danmu.$restTime}s linear 0s`; break; case 1: case 2: ref.style.left = "50%"; ref.style.marginLeft = `-${ref.clientWidth / 2}px`; break; } } release(operation) { const { danmu, ref } = operation; if (ref && danmu.$ref === ref) { ref.style.cssText = this.owner.constructor.cssText; ref.style.visibility = "hidden"; this.owner.$refs.push(ref); danmu.$ref = null; } operation.ref = null; } clear() { this.destroy(); this.owner.$refs = []; this.owner.$danmuku.textContent = ""; } destroy() { for (const node of this.nodes) { if (node.parentElement) node.parentElement.removeChild(node); } for (const danmu of this.owner.queue) danmu.$ref = null; this.owner.$refs.length = 0; this.nodes.clear(); } } function getDanmuTop({ target, visibles, clientWidth, clientHeight, marginBottom, marginTop, antiOverlap }) { const maxTop = clientHeight - marginBottom; const danmus = visibles.filter((item) => item.mode === target.mode && item.top <= maxTop).sort((prev, next) => prev.top - next.top); if (danmus.length === 0) { if (target.mode === 2) { return maxTop - target.height; } else { return marginTop; } } danmus.unshift({ type: "top", top: 0, left: 0, right: 0, height: marginTop, width: clientWidth, speed: 0, distance: clientWidth }); danmus.push({ type: "bottom", top: maxTop, left: 0, right: 0, height: marginBottom, width: clientWidth, speed: 0, distance: clientWidth }); if (target.mode === 2) { for (let index = danmus.length - 2; index >= 0; index -= 1) { const item = danmus[index]; const prev = danmus[index + 1]; const itemBottom = item.top + item.height; const diff = prev.top - itemBottom; if (diff >= target.height) { return prev.top - target.height; } } } else { for (let index = 1; index < danmus.length; index += 1) { const item = danmus[index]; const prev = danmus[index - 1]; const prevBottom = prev.top + prev.height; const diff = item.top - prevBottom; if (diff >= target.height) { return prevBottom; } } } const topMap = []; for (let index = 1; index < danmus.length - 1; index += 1) { const item = danmus[index]; if (topMap.length) { const last = topMap[topMap.length - 1]; if (last[0].top === item.top) { last.push(item); } else { topMap.push([item]); } } else { topMap.push([item]); } } if (antiOverlap) { switch (target.mode) { case 0: { const result = topMap.find((list) => { return list.every((danmu) => { if (clientWidth < danmu.distance) return false; if (target.speed < danmu.speed) return true; const overlapTime = danmu.right / (target.speed - danmu.speed); if (overlapTime > danmu.time) return true; return false; }); }); return result && result[0] ? result[0].top : void 0; } // 静止弹幕没有重叠问题 case 1: case 2: return void 0; } } else { switch (target.mode) { case 0: topMap.sort((prev, next) => { const nextMinRight = Math.min(...next.map((item) => item.right)); const prevMinRight = Math.min(...prev.map((item) => item.right)); return nextMinRight * next.length - prevMinRight * prev.length; }); break; case 1: case 2: topMap.sort((prev, next) => { const nextMaxWidth = Math.max(...next.map((item) => item.width)); const prevMaxWidth = Math.max(...prev.map((item) => item.width)); return prevMaxWidth * prev.length - nextMaxWidth * next.length; }); break; } return topMap[0][0].top; } } class SamplingWindow { constructor() { this.rows = /* @__PURE__ */ new Set(); this.waiting = /* @__PURE__ */ new Set(); this.visibility = () => this.clear(); } begin(owner) { if (this.time === void 0) this.capture(owner, [], owner.art.currentTime); } capture(owner, rows, time) { const doc = owner.$player.ownerDocument; if (doc !== this.document) { this.document?.removeEventListener("visibilitychange", this.visibility); this.document = doc; doc?.addEventListener("visibilitychange", this.visibility); this.clear(); } if (!Number.isFinite(time) || doc?.visibilityState === "hidden" || owner.art.video?.seeking) { this.clear(); return rows; } const previousTime = this.time; const previousRows = this.rows; const previousWaiting = this.waiting; this.time = time; this.rows = new Set(rows); this.waiting = new Set(owner.states.wait); if (previousTime === void 0 || time <= previousTime) return rows; const missed = owner.states.wait.filter((row) => previousWaiting.has(row) && !previousRows.has(row) && row.time >= previousTime - 0.1 && row.time < time - 0.1); for (const row of missed) this.rows.add(row); return [...rows.filter((row) => row.$state === "ready"), ...missed, ...rows.filter((row) => row.$state !== "ready")]; } clear() { this.time = void 0; this.rows.clear(); this.waiting.clear(); } destroy() { this.document?.removeEventListener("visibilitychange", this.visibility); this.document = void 0; this.clear(); } } class SchedulingBuffer { constructor() { this.pending = /* @__PURE__ */ new Set(); this.reserved = /* @__PURE__ */ new Set(); } capture(items, failed) { for (const item of items) { if (!this.reserved.has(item) && !failed.has(item)) this.pending.add(item); } } take() { const ready = []; const waiting = []; for (const item of this.pending) { if (item.$state === "ready") ready.push(item); else if (item.$state === "wait") waiting.push(item); } this.pending.clear(); const batch = [...ready, ...waiting]; this.reserved = new Set(batch); return batch; } complete() { this.reserved.clear(); } clear() { this.pending.clear(); this.reserved.clear(); } } const cancelled = /* @__PURE__ */ Symbol("cancelled danmuku frame"); class Scheduler { constructor(owner) { this.owner = owner; this.generation = 0; this.starts = 0; this.frame = null; this.operation = null; this.closed = false; this.running = false; this.fault = false; this.failedItems = /* @__PURE__ */ new Set(); this.buffer = new SchedulingBuffer(); this.sampling = new SamplingWindow(); } active(operation) { const owner = this.owner; return !this.closed && !this.fault && !owner.art.isDestroy && !owner.isStop && !owner.isHide && operation.generation === this.generation; } release(operation) { this.owner.renderer.release(operation); } invalidate() { this.generation++; if (this.frame !== null) window.cancelAnimationFrame(this.frame); this.frame = null; this.owner.timer = null; if (this.operation) { const operation = this.operation; this.operation = null; operation.cancel(); this.release(operation); } this.owner.workerClient?.cancel(); this.failedItems.clear(); this.buffer.clear(); this.sampling.clear(); } report(error) { try { this.owner.art.emit("artplayerPluginDanmuku:error", error); } catch (listenerError) { console.warn("Failed to report danmuku scheduling error:", listenerError); } } fail(error) { if (this.closed || this.fault) return; this.fault = true; this.invalidate(); this.report(error); } recover() { if (this.closed || this.owner.art.isDestroy) return; this.failedItems.clear(); if (this.fault) { try { this.owner.workerClient?.dispose(); this.owner.createWorker(); this.fault = false; } catch (error) { this.report(error); } } } schedule() { const owner = this.owner; if (!this.running || this.closed || this.fault || owner.art.isDestroy || owner.isStop) return; try { const video = owner.art.video; const started = owner.art.playing || video?.paused === false && !video.ended && video.readyState > 2; if (started && !owner.isHide) this.sampling.begin(owner); } catch (error) { this.fail(error); return; } if (this.frame !== null) return; this.frame = window.requestAnimationFrame(() => { this.frame = null; owner.timer = null; if (this.closed || this.fault || owner.isStop || owner.art.isDestroy) return; const generation = this.generation; try { if (owner.art.playing && !owner.isHide) { owner.filter("emit", (danmu) => { const now = Date.now(); danmu.$restTime -= (now - danmu.$lastStartTime) / 1e3; danmu.$lastStartTime = now; if (danmu.$restTime <= 0) owner.makeWait(danmu); }); const time = owner.art.currentTime; const readys2 = owner.readys; if (generation !== this.generation) return; this.buffer.capture(this.sampling.capture(owner, readys2, time), this.failedItems); } } catch (error) { if (generation === this.generation) this.fail(error); return; } this.schedule(); if (this.operation || !owner.art.playing || owner.isHide) return; const readys = this.buffer.take(); if (!readys.length) return; let cancel; const cancellation = new Promise((resolve) => cancel = () => resolve(cancelled)); const operation = { generation: this.generation, cancel, wait: (value) => Promise.race([value, cancellation]), ref: null }; this.operation = operation; return this.run(operation, readys).catch((error) => { if (this.active(operation)) this.fail(error); }).finally(() => { this.release(operation); if (this.operation === operation) { this.operation = null; this.buffer.complete(); } }); }); owner.timer = this.frame; } async run(operation, readys) { const owner = this.owner; if (!owner.art.playing || !this.active(operation)) return; for (const danmu of readys) { if (!this.active(operation)) return; if (this.failedItems.has(danmu)) continue; let state; try { state = await operation.wait(owner.option.beforeVisible(danmu)); } catch (error) { if (!this.active(operation)) return; this.failedItems.add(danmu); this.report(error); continue; } if (!this.active(operation)) return; if (!state) continue; const { clientWidth, clientHeight } = owner.$player; const ref = owner.renderer.prepare(danmu, operation); if (!this.active(operation)) return; danmu.$lastStartTime = Date.now(); danmu.$restTime = owner.speed; const distance = clientWidth + ref.clientWidth; const request = { type: "getDanmuTop", target: { mode: danmu.mode, height: ref.clientHeight, speed: distance / danmu.$restTime }, visibles: owner.visibles, antiOverlap: owner.option.antiOverlap, clientWidth, clientHeight, // Valid margins are numeric. Preserve the old raw fallback for unsupported strings. marginBottom: owner.marginBottom, marginTop: owner.marginTop }; const reply = request.antiOverlap ? await operation.wait(owner.postMessage(request)) : { result: getDanmuTop(request) }; const top = typeof reply === "symbol" ? void 0 : reply.result; if (!this.active(operation) || danmu.$ref !== ref) return; if (top !== void 0) { danmu.$lastStartTime = Date.now(); owner.setState(danmu, "emit"); owner.renderer.place(danmu, ref, top, distance, clientWidth); operation.ref = null; owner.art.emit("artplayerPluginDanmuku:visible", danmu); } else { owner.setState(danmu, "ready"); this.release(operation); } } } destroy() { this.closed = true; this.invalidate(); this.sampling.destroy(); } } let nextRequest = 0; class WorkerClient { constructor(createWorker, onFailure) { this.pending = /* @__PURE__ */ new Map(); this.closed = false; this.failed = false; this.onFailure = onFailure; this.worker = createWorker(); try { this.worker.onmessage = (event) => { const request = this.pending.get(event.data?.id); if (!request) return; this.pending.delete(event.data.id); request.resolve(event.data); }; this.worker.onerror = (event) => { event.preventDefault?.(); this.fail(event.error || event); }; this.worker.onmessageerror = (event) => this.fail(event.error || event); } catch (error) { try { this.dispose(); } catch { } throw error; } } request(message) { message.id = ++nextRequest; const { id } = message; if (this.closed || this.failed) return Promise.resolve({ id, result: void 0 }); return new Promise((resolve, reject) => { this.pending.set(id, { resolve, reject }); try { this.worker.postMessage(message); } catch (error) { this.fail(error); } }); } fail(error) { if (this.closed || this.failed) return; this.failed = true; for (const request of this.pending.values()) request.reject(error); this.pending.clear(); try { this.dispose(); } catch { } this.onFailure(error); } cancel() { for (const [id, request] of this.pending) request.resolve({ id, result: void 0 }); this.pending.clear(); } dispose() { if (this.closed) return; this.closed = true; this.cancel(); let failed = false; let error; const attempt = (callback) => { try { callback(); } catch (failure) { if (!failed) error = failure; failed = true; } }; for (const name of ["onmessage", "onerror", "onmessageerror"]) { attempt(() => this.worker[name] = null); } attempt(() => this.worker.terminate()); if (failed) throw error; } } const jsContent = '/*!\n * artplayer-plugin-danmuku.js v6.0.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.worker = null; this.workerClient = null; this.renderer = new Renderer(this); this.scheduler = new Scheduler(this); this.option = Danmuku.option; this.states = { wait: [], ready: [], emit: [], stop: [] }; 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); this.seek = this.seek.bind(this); inputActive(this); art.on("destroy", this.destroy); try { this.config(option, true); if (!inputActive(this)) return; this.createWorker(); } catch (error) { art.off("destroy", this.destroy); cancelInputs(this); this.scheduler.destroy(); throw error; } 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("resize", this.resize); art.on("video:seeking", this.seek); this.load().catch((error) => console.warn("Failed to load initial danmuku:", error)); } // 默认配置 static get option() { return defaultOption(); } // 配置校验 static get scheme() { return optionScheme(); } // 初始弹幕样式 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() { return this.renderer.acquire(); } // 获取准备好发送的弹幕 get readys() { return readyItems(this); } // 可见的弹幕的数据,用于计算下一个弹幕的top值 get visibles() { return this.renderer.visibles; } // 计算弹幕速度 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; const task = beginInput(this, danmuku === void 0); try { if (!task.active()) return this; const target = danmuku || this.option.danmuku; const input = readInput(target, task); const danmus = input.asynchronous ? await task.wait(input.value) : input.value; if (!task.active()) return this; errorHandle(Array.isArray(danmus), "Danmuku need return an array as result"); const rows = danmus; if (danmuku === void 0) { this.reset(); if (!task.active()) return this; this.queue = []; this.states = { wait: [], ready: [], emit: [], stop: [] }; this.renderer.clear(); } for (let index = 0; index < rows.length; index++) { if (!task.active()) return this; const danmu = rows[index]; await task.emit(danmu); } if (task.active()) this.art.emit("artplayerPluginDanmuku:loaded", this.queue); } catch (error) { if (!task.active()) return this; this.art.emit("artplayerPluginDanmuku:error", error); throw error; } finally { task.finish(); } return this; } // 把原始弹幕转换到弹幕队列 async emit(danmu) { const { clamp } = this.utils; if (!inputActive(this)) return this; 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 === 0) { 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; if (!inputActive(this)) 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 = optionChanged(this.option, option); if (!changed && !isInit) return this; const next = normalizeOption(this.option, option, { defaults: Danmuku.option, validate: (value) => this.validator(value, Danmuku.scheme), clamp, mount: $controlsCenter }); if (!inputActive(this)) return this; const beforeVisibleChanged = this.option.beforeVisible !== next.beforeVisible; this.option = next; if (beforeVisibleChanged) { this.scheduler.invalidate(); this.scheduler.schedule(); } if (option.fontSize) { this.reset(); if (!inputActive(this)) return this; } if (this.option.visible) { this.show(); } else { this.hide(); } if (inputActive(this)) this.art.emit("artplayerPluginDanmuku:config", this.option); return this; } // 计算DOM的left值,受到旋屏影响 getLeft($ref) { return this.renderer.left($ref); } // 复杂运算交给 Web Worker 处理 // Keep the historical empty default; unknown message types receive no reply. postMessage(message = {}) { return this.workerClient.request(message); } createWorker() { this.workerClient = new WorkerClient(() => new WorkerWrapper(), (error) => this.scheduler.fail(error)); this.worker = this.workerClient.worker; } // 根据状态获取弹幕 filter(state, callback) { return filterState(this, state, callback); } // 设置弹幕状态 setState(danmu, state) { setItemState(this, danmu, state); } // 重置弹幕到wait状态,回收弹幕DOM节点 makeWait(danmu) { this.renderer.makeWait(danmu); } // 实时更新弹幕 update() { this.scheduler.running = true; this.scheduler.schedule(); return this; } // 重置正在显示的弹幕: stop/emit 状态的弹幕 resize() { this.renderer.resize(); } // 继续弹幕 continue() { this.renderer.continue(); return this; } // 暂停弹幕 suspend() { this.renderer.suspend(); return this; } stop() { this.isStop = true; this.scheduler.running = false; this.scheduler.invalidate(); this.suspend(); this.art.emit("artplayerPluginDanmuku:stop"); return this; } start() { if (this.scheduler.closed || this.art.isDestroy) return this; const start = ++this.scheduler.starts; const generation = this.scheduler.generation; this.isStop = false; this.scheduler.recover(); const obsolete = this.scheduler.closed || this.art.isDestroy || this.isStop || this.scheduler.fault || generation !== this.scheduler.generation || start !== this.scheduler.starts; if (obsolete) return this; this.continue(); this.update(); this.art.emit("artplayerPluginDanmuku:start"); return this; } reset() { this.scheduler.invalidate(); this.queue.forEach((danmu) => this.makeWait(danmu)); this.art.emit("artplayerPluginDanmuku:reset"); this.scheduler.recover(); this.scheduler.schedule(); return this; } seek() { this.scheduler.invalidate(); this.scheduler.schedule(); } 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.scheduler.invalidate(); this.$danmuku.style.opacity = 0; this.option.visible = false; this.art.emit("artplayerPluginDanmuku:hide"); this.scheduler.schedule(); return this; } destroy() { cancelInputs(this); this.scheduler.destroy(); try { try { this.stop(); } finally { this.workerClient?.dispose(); } } finally { this.renderer.destroy(); 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.resize); this.art.off("video:seeking", this.seek); this.art.off("destroy", this.destroy); this.art.emit("artplayerPluginDanmuku:destroy"); } } } const MAX_HEATMAP_POINTS = 4294967295; function upperBound(values, value) { let start = 0; let end = values.length; while (start < end) { const middle = Math.floor((start + end) / 2); if (values[middle] <= value) start = middle + 1; else end = middle; } return start; } function sampleHeatmap(queue, width, duration, sampling) { if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(duration) || duration <= 0 || !Number.isFinite(sampling) || sampling <= 0 || Math.floor(width / sampling) + 1 > MAX_HEATMAP_POINTS) { return []; } const times = queue.map((item) => item.time).filter((time) => typeof time === "number" && !Number.isNaN(time)).sort((a, b) => a - b); const points = []; const gap = duration / width; if (!Number.isFinite(gap)) return []; for (let x = 0; x <= width; x += sampling) { if (points.length >= MAX_HEATMAP_POINTS || x + sampling === x) return []; const left = x * gap; const right = (x + sampling) * gap; points.push([x, upperBound(times, right) - upperBound(times, left)]); } return points; } function map(value, inMin, inMax, outMin, outMax) { return (value - inMin) * (outMax - outMin) / (inMax - inMin) + outMin; } 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 heatmapGeometry({ width, height, duration, queue, option, points: input = [] }) { if (![width, height, duration].every((value) => Number.isFinite(value) && value > 0)) return null; const defaults = { xMin: 0, xMax: width, yMin: 0, yMax: 128, scale: 0.25, opacity: 0.2, minHeight: Math.floor(height * 0.05), sampling: Math.max(1, Math.floor(width / 100)), smoothing: 0.2, flattening: 0.2 }; const configured = Object.assign({}, typeof option === "object" ? option : {}); const options = Object.assign({}, defaults, configured); for (const key of Object.keys(defaults)) { if (!Number.isFinite(options[key])) options[key] = defaults[key]; } if (options.sampling <= 0) options.sampling = defaults.sampling; if (options.xMin === options.xMax || options.yMin === options.yMax) return null; const hasCustomPoints = Array.isArray(input) && input.length > 0; const points = hasCustomPoints ? [...input] : sampleHeatmap(queue, width, duration, options.sampling); if (!points.length || !points.every((point) => Array.isArray(point) && Number.isFinite(point[0]) && Number.isFinite(point[1]))) { return null; } const lastPoint = points[points.length - 1]; if (lastPoint[0] !== width) { if (points.length === MAX_HEATMAP_POINTS) return null; points.push([width, lastPoint[1]]); } let yMin = Infinity; let yMax = -Infinity; for (const point of points) { yMin = Math.min(yMin, point[1]); yMax = Math.max(yMax, point[1]); } const yMid = (yMin + yMax) / 2; for (const point of points) { const y = point[1]; point[1] = y * (y > yMid ? 1 + options.scale : 1 - options.scale) + options.minHeight; } const automatic = !hasCustomPoints && !Number.isFinite(configured.yMin) && !Number.isFinite(configured.yMax); let peak = 0; for (const point of points) peak = Math.max(peak, point[1]); const fitted = automatic && peak > defaults.yMax / 4; if (fitted) options.yMax = peak * 4; const boundY = (value) => fitted ? Math.min(height, Math.max(height * 0.75, value)) : value; const controlPoint = (current, previous, next, reverse) => { const geometry = line(previous || current, next || current); const flat = map(Math.cos(geometry.angle) * options.flattening, 0, 1, 1, 0); const angle = geometry.angle * flat + (reverse ? Math.PI : 0); const length = geometry.length * options.smoothing; return [current[0] + Math.cos(angle) * length, boundY(current[1] + Math.sin(angle) * length)]; }; const positions = points.map((point) => [map(point[0], options.xMin, options.xMax, 0, width), boundY(map(point[1], options.yMin, options.yMax, height, 0))]); if (!positions.every((point) => point.every(Number.isFinite))) return null; const path = positions.map((point, index, all) => { if (index === 0) return `M ${all[all.length - 1][0]},${height} L ${point[0]},${height} L ${point[0]},${point[1]}`; const start = controlPoint(all[index - 1], all[index - 2], point); const end = controlPoint(point, all[index - 1], all[index + 1], true); return `C ${start[0]},${start[1]} ${end[0]},${end[1]} ${point[0]},${point[1]}${index === all.length - 1 ? " z" : ""}`; }).join(" "); if (/NaN|Infinity/u.test(path)) return null; return { path, opacity: options.opacity, width, height }; } let nextGradient = 0; function heatmap(art, danmuku, option) { if (art.isDestroy) return; const { query } = art.constructor.utils; let gradient; const subscriptions = []; let element; let start; let stop; let closed = false; const active = () => !closed && !art.isDestroy; function listen(name, callback) { const listener = callback; subscriptions.push([name, listener]); art.on(name, listener); } function dispose(removeControl) { if (closed) return; closed = true; start = null; stop = null; let failure; let failed = false; const attempt = (callback) => { try { callback(); } catch (error) { if (!failed) failure = error; failed = true; } }; for (const [name, callback] of subscriptions.splice(0)) attempt(() => art.off(name, callback)); if (removeControl && element && art.controls.heatmap === element) attempt(() => art.controls.remove("heatmap")); element = void 0; if (failed) throw failure; } function progress(value) { if (active() && start && stop) { start.setAttribute("offset", `${value * 100}%`); stop.setAttribute("offset", `${value * 100}%`); } } function draw(points = []) { if (!active()) return; start = null; stop = null; const target = element; target.innerHTML = ""; if (art.option.isLive) return; const shape = heatmapGeometry({ width: target.offsetWidth, height: target.offsetHeight, duration: art.duration, queue: danmuku.queue, option, points }); if (!shape || !active()) return; const document2 = target.ownerDocument; if (!gradient) gradient = document2 ? "heatmap-solids" : `heatmap-solids-${++nextGradient}`; while (document2?.getElementById(gradient)) gradient = `heatmap-solids-${++nextGradient}`; target.innerHTML = ` `; start = query("#heatmap-start", target); stop = query("#heatmap-stop", target); progress(art.played); } function update(points) { try { draw(points); } catch (error) { try { dispose(true); } catch { } throw error; } } listen("destroy", () => dispose(true)); try { art.controls.add({ name: "heatmap", position: "top", html: "", style: { position: "absolute", top: "-100px", left: "0px", right: "0px", height: "100px", width: "100%", pointerEvents: "none" }, beforeUnmount() { dispose(false); }, mounted(value) { element = value; if (closed) { if (art.controls.heatmap === value) art.controls.remove("heatmap"); element = void 0; return; } if (!active()) { dispose(true); return; } listen("video:timeupdate", () => progress(art.played)); listen("setBar", (type, percentage) => { if (type === "played") progress(percentage); }); listen("ready", () => update()); listen("resize", () => update()); listen("artplayerPluginDanmuku:loaded", () => update()); listen("artplayerPluginDanmuku:points", (points) => update(points)); } }); } catch (error) { try { dispose(true); } catch { } throw error; } return () => dispose(true); } const properties = /* @__PURE__ */ new WeakMap(); class SettingLifecycle { constructor(art) { this.art = art; this.closed = false; this.disposers = /* @__PURE__ */ new Set(); this.cancelled = new Promise((resolve) => this.cancel = resolve); } get active() { return !this.closed && !this.art.isDestroy; } own(dispose) { if (this.closed) dispose(); else this.disposers.add(dispose); } write(host, key, value, apply = (value2) => host[key] = value2) { const target = host; if (!this.active) return; const priority = () => typeof target.getPropertyPriority === "function" ? target.getPropertyPriority(key) : void 0; let keys = properties.get(target); if (!keys) { keys = /* @__PURE__ */ new Map(); properties.set(target, keys); } let entry = keys.get(key); if (!entry) { entry = { original: target[key], present: key in target || target[key] !== void 0, priority: priority(), owners: /* @__PURE__ */ new Map(), apply }; keys.set(key, entry); } if (!entry.owners.has(this)) { this.own(() => { const owners = [...entry.owners.keys()]; const current = owners[owners.length - 1] === this; entry.owners.delete(this); if (current && target[key] === entry.last && priority() === entry.lastPriority) { const values = [...entry.owners.values()]; if (values.length) { entry.apply(values[values.length - 1]); entry.last = target[key]; entry.lastPriority = priority(); } else if (entry.priority !== void 0) { target.setProperty(key, entry.original, entry.priority); } else if (entry.present) { entry.apply(entry.original); } else { delete target[key]; } } if (!entry.owners.size) { keys.delete(key); if (!keys.size) properties.delete(target); } }); } entry.owners.delete(this); entry.owners.set(this, value); entry.apply(value); entry.last = target[key]; entry.lastPriority = priority(); } on(name, callback) { if (!this.active) return; const lifecycle = this; function listener(...args) { if (lifecycle.active) return callback.apply(this, args); } const dispose = () => this.art.off(name, listener); this.own(dispose); try { this.art.on(name, listener); if (!this.active) dispose(); } catch (error) { this.release(dispose); throw error; } } proxy(target, name, callback) { if (!this.active) return; const lifecycle = this; function listener(...args) { if (lifecycle.active) return callback.apply(this, args); } let remove; const dispose = () => { if (typeof remove === "function") { const registry = this.art.events; if (typeof registry?.remove === "function" && registry.destroyEvents?.has(remove)) registry.remove(remove); else remove(); } else { target.removeEventListener?.(name, listener); } }; this.own(dispose); try { remove = this.art.proxy(target, name, listener); if (!this.active) dispose(); } catch (error) { this.release(dispose); throw error; } } wait(value) { return Promise.race([value, this.cancelled]); } release(dispose) { try { dispose(); } catch (error) { console.warn("Failed to dispose danmuku setting resource:", error); } } close() { if (this.closed) return; this.closed = true; this.cancel(); for (const dispose of this.disposers) { this.release(dispose); } this.disposers.clear(); } } async function emitSetting(setting) { const { lifecycle, template: { $input } } = setting; if (!lifecycle.active) return; const text = $input.value.trim(); if (!text.length || setting.isLock || setting.emitting) return; const danmu = { text, mode: setting.option.mode, color: setting.option.color, time: setting.art.currentTime }; const report = (error) => { if (lifecycle.active) console.error("Error emitting danmuku:", error); }; try { setting.emitting = true; const state = await lifecycle.wait(setting.option.beforeEmit(danmu)); if (!lifecycle.active) return; setting.emitting = false; if (state !== true) return; danmu.border = true; delete danmu.time; Promise.resolve(setting.danmuku.emit(danmu)).catch(report); if (!lifecycle.active) return; $input.value = ""; setting.lock(); } catch (error) { report(error); if (lifecycle.active) setting.emitting = false; } } function lockSetting(setting) { if (!setting.lifecycle.active) return; const { addClass } = setting.utils; const { $send } = setting.template; clearTimeout(setting.timer); setting.isLock = true; let time = setting.option.lockTime; $send.textContent = time; addClass($send, "apd-lock"); const loop = () => { setting.timer = setTimeout(() => { if (!setting.lifecycle.active) return; if (time === 0) { setting.unlock(); } else { time -= 1; $send.textContent = time; loop(); } }, 1e3); }; loop(); } function unlockSetting(setting) { clearTimeout(setting.timer); setting.timer = null; setting.isLock = false; if (!setting.lifecycle.active) return; const { removeClass } = setting.utils; const { $send } = setting.template; $send.textContent = "发送"; removeClass($send, "apd-lock"); } function createSlider({ min, max, container, findIndex, onChange, steps = [] }) { const { lifecycle } = this; if (!lifecycle.active) return { reset() { } }; 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; lifecycle.own(() => isDroging = false); function reset(index) { if (!lifecycle.active) return; if (index === void 0) 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); } } lifecycle.proxy(container, "click", (event) => { updateLeft.call(this, event); }); lifecycle.proxy(container, "pointerdown", (event) => { isDroging = event.button === 0; }); lifecycle.on("document:pointermove", (event) => { if (isDroging) { updateLeft.call(this, event); } }); lifecycle.on("document:pointerup", (event) => { if (isDroging) { isDroging = false; updateLeft.call(this, event); } }); return { reset }; } 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 = ''; function settingIcons() { 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 }; } function settingTemplate() { 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("")}
发送
`; } 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"; if (typeof document !== "undefined") { const id = "artplayer-plugin-danmuku"; const pending = /* @__PURE__ */ Symbol.for("artplayer-plugin-danmuku.pending-style"); const styleDocument = document; let $style2 = document.getElementById(id) || styleDocument[pending]; if (!$style2) { $style2 = document.createElement("style"); $style2.id = id; if (document.readyState === "loading") { styleDocument[pending] = $style2; const ready = () => { document.removeEventListener("DOMContentLoaded", ready); if (styleDocument[pending] === $style2) delete styleDocument[pending]; const existing = document.getElementById(id); if (existing) existing.textContent = $style2.textContent; else (document.head || document.documentElement).appendChild($style2); }; document.addEventListener("DOMContentLoaded", ready, { once: true }); } else { (document.head || document.documentElement).appendChild($style2); } } $style2.textContent = style; } class Setting { constructor(art, danmuku) { this.art = art; this.danmuku = danmuku; this.utils = art.constructor.utils; const { setStyle } = this.utils; const { $controlsCenter } = art.template; 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.lifecycle = new SettingLifecycle(art); this.destroy = this.destroy.bind(this); this.lifecycle.own(() => art.off("destroy", this.destroy)); try { art.on("destroy", this.destroy); if (!this.lifecycle.active) return; this.lifecycle.write($controlsCenter.style, "display", "flex", (value) => setStyle($controlsCenter, "display", value)); this.createTemplate(); if (!this.lifecycle.active) return; this.createSliders(); if (!this.lifecycle.active) return; this.createEvents(); this.mount(this.option.mount); if (!this.lifecycle.active) return; this.lifecycle.on("resize", () => this.resize()); this.lifecycle.on("fullscreen", (state) => this.onFullscreen(state)); this.lifecycle.on("fullscreenWeb", (state) => this.onFullscreen(state)); this.lifecycle.proxy(this.template.$config, "mouseenter", () => { this.onMouseEnter({ $control: this.template.$config, $panel: this.template.$configPanel }); }); this.lifecycle.proxy(this.template.$style, "mouseenter", () => { this.onMouseEnter({ $control: this.template.$style, $panel: this.template.$stylePanel }); }); } catch (error) { try { this.destroy(); } catch (cleanupError) { console.warn("Failed to roll back danmuku setting:", cleanupError); } throw error; } } static get icons() { return settingIcons(); } get option() { return this.danmuku.option; } get outside() { return this.template.$mount !== this.template.$controlsCenter; } get TEMPLATE() { return settingTemplate.call(this); } 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) { if (!this.lifecycle.active) return; const { append } = this.utils; const children = [...el.children]; if (children.includes(target)) return; append(el, target); } setData(key, value) { if (!this.lifecycle.active) return; const { $player } = this.art.template; const { $mount } = this.template; this.lifecycle.write($player.dataset, key, value); if (this.outside) { this.lifecycle.write($mount.dataset, key, value); } } createTemplate() { const { createElement, tooltip } = this.utils; const $danmuku = createElement("div"); this.template.$danmuku = $danmuku; $danmuku.className = "artplayer-plugin-danmuku"; $danmuku.innerHTML = this.TEMPLATE; 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.lifecycle.on("artplayerPluginDanmuku:show", () => { tooltip($toggle, "关闭弹幕"); }); this.lifecycle.on("artplayerPluginDanmuku:hide", () => { tooltip($toggle, "打开弹幕"); }); } createEvents() { const { $toggle, $configModes, $styleModes, $colors, $antiOverlap, $syncVideo, $send, $input } = this.template; this.lifecycle.proxy($toggle, "click", () => { this.danmuku.config({ visible: !this.option.visible }); this.reset(); }); this.lifecycle.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.lifecycle.proxy($antiOverlap, "click", () => { this.danmuku.config({ antiOverlap: !this.option.antiOverlap }); this.reset(); }); this.lifecycle.proxy($syncVideo, "click", () => { this.danmuku.config({ synchronousPlayback: !this.option.synchronousPlayback }); this.reset(); }); this.lifecycle.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.lifecycle.proxy($colors, "click", (event) => { const $color = event.target.closest(".apd-color"); if (!$color) return; this.danmuku.config({ color: $color.dataset.color }); this.reset(); }); this.lifecycle.proxy($send, "click", () => this.emit()); this.lifecycle.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(option) { return createSlider.call(this, option); } onFullscreen(state) { if (!this.lifecycle.active) return; 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 }) { if (!this.lifecycle.active) return; 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`; } } emit() { return emitSetting(this); } lock() { lockSetting(this); } unlock() { unlockSetting(this); } resize() { if (!this.lifecycle.active) return; 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() { if (!this.lifecycle.active) return; const { inverseClass, tooltip } = this.utils; const { $toggle, $colors } = this.template; for (const slider of Object.values(this.slider)) { slider.reset(); if (!this.lifecycle.active) return; } 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) { if (!this.lifecycle.active) return; 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(); } destroy() { if (this.lifecycle.closed) return; this.lifecycle.close(); clearTimeout(this.timer); this.timer = null; this.emitting = false; this.isLock = false; const $danmuku = this.template.$danmuku; if ($danmuku?.parentElement) $danmuku.parentElement.removeChild($danmuku); } } function artplayerPluginDanmuku(option) { return (art) => { const danmuku = new Danmuku(art, option); let setting; try { setting = art.isDestroy ? void 0 : new Setting(art, danmuku); if (!art.isDestroy && danmuku.option.heatmap) { heatmap(art, danmuku, danmuku.option.heatmap); } } catch (error) { try { setting?.destroy(); } catch { } try { danmuku.destroy(); } catch { } throw error; } 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 ? 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 };