mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-29 02:51:20 -08:00
refactor(danmuku): [PKG-DANMUKU-05] own renderer settings and heatmap resources
This commit is contained in:
1 parent
12332df221
commit
053d211fb0
38 files changed
+65630
-2420
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { defaultOption, normalizeOption, optionChanged, optionScheme } from './config'
|
||||
import { beginInput, cancelInputs, inputActive, readInput } from './input'
|
||||
import { filterState, readyItems, setItemState } from './queue'
|
||||
import Renderer from './renderer'
|
||||
import Scheduler from './scheduler'
|
||||
import WorkerClient from './worker-client'
|
||||
import DanmuWorker from './worker.js?worker&inline'
|
||||
@@ -23,6 +24,7 @@ export default class Danmuku {
|
||||
this.index = 0 // 弹幕索引
|
||||
this.worker = null
|
||||
this.workerClient = null
|
||||
this.renderer = new Renderer(this)
|
||||
this.scheduler = new Scheduler(this)
|
||||
|
||||
// 格式化后的配置项
|
||||
@@ -158,12 +160,7 @@ export default class Danmuku {
|
||||
|
||||
// 获取弹幕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
|
||||
return this.renderer.acquire()
|
||||
}
|
||||
|
||||
// 获取准备好发送的弹幕
|
||||
@@ -173,34 +170,7 @@ export default class Danmuku {
|
||||
|
||||
// 可见的弹幕的数据,用于计算下一个弹幕的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
|
||||
return this.renderer.visibles
|
||||
}
|
||||
|
||||
// 计算弹幕速度
|
||||
@@ -234,8 +204,7 @@ export default class Danmuku {
|
||||
return this
|
||||
this.queue = [] // 清空弹幕队列
|
||||
this.states = { wait: [], ready: [], emit: [], stop: [] } // 清空弹幕状态池
|
||||
this.$refs = [] // 清空弹幕DOM节点池
|
||||
this.$danmuku.textContent = '' // 清空弹幕层
|
||||
this.renderer.clear()
|
||||
}
|
||||
|
||||
// 逐个验证原始弹幕并转换到弹幕队列
|
||||
@@ -391,8 +360,7 @@ export default class Danmuku {
|
||||
|
||||
// 计算DOM的left值,受到旋屏影响
|
||||
getLeft($ref) {
|
||||
const rect = $ref.getBoundingClientRect()
|
||||
return this.isRotate ? rect.top : rect.left
|
||||
return this.renderer.left($ref)
|
||||
}
|
||||
|
||||
// 复杂运算交给 Web Worker 处理
|
||||
@@ -417,16 +385,7 @@ export default class Danmuku {
|
||||
|
||||
// 重置弹幕到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
|
||||
}
|
||||
this.renderer.makeWait(danmu)
|
||||
}
|
||||
|
||||
// 实时更新弹幕
|
||||
@@ -438,76 +397,18 @@ export default class Danmuku {
|
||||
|
||||
// 重置正在显示的弹幕: stop/emit 状态的弹幕
|
||||
resize() {
|
||||
const { clientWidth } = this.$player
|
||||
|
||||
this.filter('stop', (danmu) => {
|
||||
switch (danmu.mode) {
|
||||
// 滚动的弹幕
|
||||
case 0:
|
||||
danmu.$ref.style.left = `${clientWidth}px`
|
||||
break
|
||||
default:
|
||||
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
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
this.renderer.resize()
|
||||
}
|
||||
|
||||
// 继续弹幕
|
||||
continue() {
|
||||
const { clientWidth } = this.$player
|
||||
this.filter('stop', (danmu) => {
|
||||
this.setState(danmu, 'emit') // 转换为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
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
this.renderer.continue()
|
||||
return this
|
||||
}
|
||||
|
||||
// 暂停弹幕
|
||||
suspend() {
|
||||
const { clientWidth } = this.$player
|
||||
this.filter('emit', (danmu) => {
|
||||
this.setState(danmu, 'stop') // 转换为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
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
this.renderer.suspend()
|
||||
return this
|
||||
}
|
||||
|
||||
@@ -581,6 +482,7 @@ export default class Danmuku {
|
||||
}
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { MAX_HEATMAP_POINTS, sampleHeatmap } from './heatmap-sampling'
|
||||
|
||||
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) }
|
||||
}
|
||||
|
||||
export 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 options = Object.assign({}, defaults, typeof option === 'object' ? option : {})
|
||||
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 points = Array.isArray(input) && input.length ? [...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
|
||||
// Historical points events copy only the outer array: callers observe these y writes.
|
||||
for (const point of points) {
|
||||
const y = point[1]
|
||||
point[1] = y * (y > yMid ? 1 + options.scale : 1 - options.scale) + options.minHeight
|
||||
}
|
||||
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, current[1] + Math.sin(angle) * length]
|
||||
}
|
||||
const positions = points.map(point => [map(point[0], options.xMin, options.xMax, 0, width), 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 }
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
export const MAX_HEATMAP_POINTS = 0xFFFFFFFF
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
export 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 []
|
||||
// Keep the old floating-point x progression and strict-left/inclusive-right bins.
|
||||
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
|
||||
}
|
||||
@@ -1,186 +1,146 @@
|
||||
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
|
||||
},
|
||||
)
|
||||
},
|
||||
}
|
||||
import { heatmapGeometry } from './heatmap-geometry'
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
let nextGradient = 0
|
||||
|
||||
export default 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
|
||||
|
||||
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 = `
|
||||
<svg viewBox="0 0 ${svg.w} ${svg.h}">
|
||||
<defs>
|
||||
<linearGradient id="heatmap-solids" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:var(--art-theme);stop-opacity:${options.opacity}" />
|
||||
<stop offset="0%" style="stop-color:var(--art-theme);stop-opacity:${options.opacity}" id="heatmap-start" />
|
||||
<stop offset="0%" style="stop-color:var(--art-progress-color);stop-opacity:1" id="heatmap-stop" />
|
||||
<stop offset="100%" style="stop-color:var(--art-progress-color);stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill="url(#heatmap-solids)" d="${pathD}"></path>
|
||||
</svg>
|
||||
`
|
||||
|
||||
$start = query('#heatmap-start', $heatmap)
|
||||
$stop = query('#heatmap-stop', $heatmap)
|
||||
$start.setAttribute('offset', `${art.played * 100}%`)
|
||||
$stop.setAttribute('offset', `${art.played * 100}%`)
|
||||
const active = () => !closed && !art.isDestroy
|
||||
function listen(name, callback) {
|
||||
subscriptions.push([name, callback])
|
||||
art.on(name, callback)
|
||||
}
|
||||
function dispose(removeControl) {
|
||||
if (closed)
|
||||
return
|
||||
closed = true
|
||||
start = null
|
||||
stop = null
|
||||
let failure
|
||||
let failed = false
|
||||
const attempt = (callback) => {
|
||||
try {
|
||||
callback()
|
||||
}
|
||||
|
||||
art.on('video:timeupdate', () => {
|
||||
if ($start && $stop) {
|
||||
$start.setAttribute('offset', `${art.played * 100}%`)
|
||||
$stop.setAttribute('offset', `${art.played * 100}%`)
|
||||
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 = undefined
|
||||
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
|
||||
element.innerHTML = ''
|
||||
if (art.option.isLive)
|
||||
return
|
||||
const shape = heatmapGeometry({ width: element.offsetWidth, height: element.offsetHeight, duration: art.duration, queue: danmuku.queue, option, points })
|
||||
if (!shape || !active())
|
||||
return
|
||||
const document = element.ownerDocument
|
||||
if (!gradient)
|
||||
gradient = document ? 'heatmap-solids' : `heatmap-solids-${++nextGradient}`
|
||||
while (document?.getElementById(gradient))
|
||||
gradient = `heatmap-solids-${++nextGradient}`
|
||||
element.innerHTML = `
|
||||
<svg viewBox="0 0 ${shape.width} ${shape.height}">
|
||||
<defs>
|
||||
<linearGradient id="${gradient}" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:var(--art-theme);stop-opacity:${shape.opacity}" />
|
||||
<stop offset="0%" style="stop-color:var(--art-theme);stop-opacity:${shape.opacity}" id="heatmap-start" />
|
||||
<stop offset="0%" style="stop-color:var(--art-progress-color);stop-opacity:1" id="heatmap-stop" />
|
||||
<stop offset="100%" style="stop-color:var(--art-progress-color);stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill="url(#${gradient})" d="${shape.path}"></path>
|
||||
</svg>
|
||||
`
|
||||
start = query('#heatmap-start', element)
|
||||
stop = query('#heatmap-stop', element)
|
||||
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 = undefined
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
art.on('setBar', (type, percentage) => {
|
||||
if ($start && $stop && type === 'played') {
|
||||
$start.setAttribute('offset', `${percentage * 100}%`)
|
||||
$stop.setAttribute('offset', `${percentage * 100}%`)
|
||||
if (!active()) {
|
||||
dispose(true)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
art.on('ready', () => update())
|
||||
art.on('resize', () => update())
|
||||
art.on('artplayerPluginDanmuku:loaded', () => update())
|
||||
art.on('artplayerPluginDanmuku:points', points => update(points))
|
||||
},
|
||||
})
|
||||
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)
|
||||
}
|
||||
@@ -5,10 +5,23 @@ import Setting from './setting'
|
||||
export default function artplayerPluginDanmuku(option) {
|
||||
return (art) => {
|
||||
const danmuku = new Danmuku(art, option)
|
||||
const setting = art.isDestroy ? undefined : new Setting(art, danmuku)
|
||||
|
||||
if (!art.isDestroy && danmuku.option.heatmap) {
|
||||
heatmap(art, danmuku, danmuku.option.heatmap)
|
||||
let setting
|
||||
try {
|
||||
setting = art.isDestroy ? undefined : 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 {
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
export default class Renderer {
|
||||
constructor(owner) {
|
||||
this.owner = owner
|
||||
this.nodes = 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
|
||||
default:
|
||||
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
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
continue() {
|
||||
const { clientWidth } = this.owner.$player
|
||||
this.owner.filter('stop', (danmu) => {
|
||||
this.owner.setState(danmu, 'emit') // 转换为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
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
suspend() {
|
||||
const { clientWidth } = this.owner.$player
|
||||
this.owner.filter('emit', (danmu) => {
|
||||
this.owner.setState(danmu, 'stop') // 转换为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
|
||||
}
|
||||
default:
|
||||
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
|
||||
default:
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -20,14 +20,7 @@ export default class Scheduler {
|
||||
}
|
||||
|
||||
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
|
||||
this.owner.renderer.release(operation)
|
||||
}
|
||||
|
||||
invalidate() {
|
||||
@@ -108,7 +101,6 @@ export default class Scheduler {
|
||||
|
||||
async run(operation) {
|
||||
const owner = this.owner
|
||||
const { setStyles } = owner.utils
|
||||
if (!owner.art.playing || !this.active(operation))
|
||||
return
|
||||
owner.filter('emit', (danmu) => {
|
||||
@@ -141,18 +133,7 @@ export default class Scheduler {
|
||||
continue
|
||||
|
||||
const { clientWidth, clientHeight } = owner.$player
|
||||
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
|
||||
setStyles(ref, danmu.style)
|
||||
const ref = owner.renderer.prepare(danmu, operation)
|
||||
if (!this.active(operation))
|
||||
return
|
||||
danmu.$lastStartTime = Date.now()
|
||||
@@ -172,25 +153,7 @@ export default class Scheduler {
|
||||
return
|
||||
if (top !== undefined) {
|
||||
owner.setState(danmu, 'emit')
|
||||
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
|
||||
default:
|
||||
break
|
||||
}
|
||||
owner.renderer.place(danmu, ref, top, distance, clientWidth)
|
||||
operation.ref = null
|
||||
owner.art.emit('artplayerPluginDanmuku:visible', danmu)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
const properties = new WeakMap()
|
||||
|
||||
export default class SettingLifecycle {
|
||||
constructor(art) {
|
||||
this.art = art
|
||||
this.closed = false
|
||||
this.disposers = 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(target, key, value, apply = value => target[key] = value) {
|
||||
if (!this.active)
|
||||
return
|
||||
const priority = () => typeof target.getPropertyPriority === 'function' ? target.getPropertyPriority(key) : undefined
|
||||
let keys = properties.get(target)
|
||||
if (!keys) {
|
||||
keys = new Map()
|
||||
properties.set(target, keys)
|
||||
}
|
||||
let entry = keys.get(key)
|
||||
if (!entry) {
|
||||
entry = {
|
||||
original: target[key],
|
||||
present: key in target || target[key] !== undefined,
|
||||
priority: priority(),
|
||||
owners: 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)
|
||||
// A later user write belongs to the host, not this settings instance.
|
||||
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 !== undefined) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
export 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
|
||||
// emit inserts synchronously; observe its Promise without delaying the UI.
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
export 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()
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
loop()
|
||||
}
|
||||
|
||||
export 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')
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
export default 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 = `
|
||||
<div class="apd-slider-line">
|
||||
<div class="apd-slider-points">
|
||||
${steps.map(() => `<div class="apd-slider-point"></div>`).join('')}
|
||||
</div>
|
||||
<div class="apd-slider-progress"></div>
|
||||
</div>
|
||||
<div class="apd-slider-dot"></div>
|
||||
<div class="apd-slider-steps">
|
||||
${steps.map(step => (step.hide ? '' : `<div class="apd-slider-step">${step.name}</div>`)).join('')}
|
||||
</div>
|
||||
`
|
||||
|
||||
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 === undefined)
|
||||
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 }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import style from './style.less?inline'
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
const id = 'artplayer-plugin-danmuku'
|
||||
const pending = Symbol.for('artplayer-plugin-danmuku.pending-style')
|
||||
let $style = document.getElementById(id) || document[pending]
|
||||
if (!$style) {
|
||||
$style = document.createElement('style')
|
||||
$style.id = id
|
||||
if (document.readyState === 'loading') {
|
||||
document[pending] = $style
|
||||
const ready = () => {
|
||||
document.removeEventListener('DOMContentLoaded', ready)
|
||||
if (document[pending] === $style)
|
||||
delete document[pending]
|
||||
const existing = document.getElementById(id)
|
||||
if (existing)
|
||||
existing.textContent = $style.textContent
|
||||
else
|
||||
(document.head || document.documentElement).appendChild($style)
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', ready, { once: true })
|
||||
}
|
||||
else {
|
||||
(document.head || document.documentElement).appendChild($style)
|
||||
}
|
||||
}
|
||||
$style.textContent = style
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import $check_off from './img/check_off.svg?raw'
|
||||
import $check_on from './img/check_on.svg?raw'
|
||||
import $config from './img/config.svg?raw'
|
||||
import $mode_0_off from './img/mode_0_off.svg?raw'
|
||||
import $mode_0_on from './img/mode_0_on.svg?raw'
|
||||
import $mode_1_off from './img/mode_1_off.svg?raw'
|
||||
import $mode_1_on from './img/mode_1_on.svg?raw'
|
||||
import $mode_2_off from './img/mode_2_off.svg?raw'
|
||||
import $mode_2_on from './img/mode_2_on.svg?raw'
|
||||
import $off from './img/off.svg?raw'
|
||||
import $on from './img/on.svg?raw'
|
||||
import $style from './img/style.svg?raw'
|
||||
|
||||
export 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,
|
||||
}
|
||||
}
|
||||
|
||||
export function settingTemplate() {
|
||||
const { option } = this
|
||||
return `
|
||||
<div class="apd-toggle">
|
||||
${$on}${$off}
|
||||
</div>
|
||||
<div class="apd-config">
|
||||
${$config}
|
||||
<div class="apd-config-panel">
|
||||
<div class="apd-config-panel-inner">
|
||||
<div class="apd-config-mode">
|
||||
按类型屏蔽
|
||||
<div class="apd-modes">
|
||||
<div data-mode="0" class="apd-mode">
|
||||
${$mode_0_off}${$mode_0_on}
|
||||
<div>滚动</div>
|
||||
</div>
|
||||
<div data-mode="1" class="apd-mode">
|
||||
${$mode_1_off}${$mode_1_on}
|
||||
<div>顶部</div>
|
||||
</div>
|
||||
<div data-mode="2" class="apd-mode">
|
||||
${$mode_2_off}${$mode_2_on}
|
||||
<div>底部</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="apd-config-other">
|
||||
<div class="apd-other apd-anti-overlap">
|
||||
${$check_on}${$check_off}
|
||||
防止弹幕重叠
|
||||
</div>
|
||||
<div class="apd-other apd-sync-video">
|
||||
${$check_on}${$check_off}
|
||||
同步视频速度
|
||||
</div>
|
||||
</div>
|
||||
<div class="apd-config-slider apd-config-opacity">
|
||||
不透明度
|
||||
<div class="apd-slider"></div>
|
||||
<div class="apd-value">未知</div>
|
||||
</div>
|
||||
<div class="apd-config-slider apd-config-margin">
|
||||
显示区域
|
||||
<div class="apd-slider"></div>
|
||||
<div class="apd-value">未知</div>
|
||||
</div>
|
||||
<div class="apd-config-slider apd-config-fontSize">
|
||||
弹幕字号
|
||||
<div class="apd-slider"></div>
|
||||
<div class="apd-value">未知</div>
|
||||
</div>
|
||||
<div class="apd-config-slider apd-config-speed">
|
||||
弹幕速度
|
||||
<div class="apd-slider"></div>
|
||||
<div class="apd-value">未知</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="apd-emitter">
|
||||
<div class="apd-style">
|
||||
${$style}
|
||||
<div class="apd-style-panel">
|
||||
<div class="apd-style-panel-inner">
|
||||
<div class="apd-style-mode">
|
||||
模式
|
||||
<div class="apd-modes">
|
||||
<div data-mode="0" class="apd-mode">
|
||||
${$mode_0_on}
|
||||
<div>滚动</div>
|
||||
</div>
|
||||
<div data-mode="1" class="apd-mode">
|
||||
${$mode_1_on}
|
||||
<div>顶部</div>
|
||||
</div>
|
||||
<div data-mode="2" class="apd-mode">
|
||||
${$mode_2_on}
|
||||
<div>底部</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="apd-style-color">
|
||||
颜色
|
||||
<div class="apd-colors">
|
||||
${this.COLOR.map(color => `<div data-color="${color}" class="apd-color" style="background-color: ${color}"></div>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input class="apd-input" placeholder="发个友善的弹幕见证当下" autocomplete="off" maxLength="${option.maxLength}" />
|
||||
<div class="apd-send">发送</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -1,16 +1,8 @@
|
||||
import $check_off from './img/check_off.svg?raw'
|
||||
import $check_on from './img/check_on.svg?raw'
|
||||
import $config from './img/config.svg?raw'
|
||||
import $mode_0_off from './img/mode_0_off.svg?raw'
|
||||
import $mode_0_on from './img/mode_0_on.svg?raw'
|
||||
import $mode_1_off from './img/mode_1_off.svg?raw'
|
||||
import $mode_1_on from './img/mode_1_on.svg?raw'
|
||||
import $mode_2_off from './img/mode_2_off.svg?raw'
|
||||
import $mode_2_on from './img/mode_2_on.svg?raw'
|
||||
import $off from './img/off.svg?raw'
|
||||
import $on from './img/on.svg?raw'
|
||||
import $style from './img/style.svg?raw'
|
||||
import style from './style.less?inline'
|
||||
import SettingLifecycle from './setting-lifecycle'
|
||||
import { emitSetting, lockSetting, unlockSetting } from './setting-send'
|
||||
import createSlider from './setting-slider'
|
||||
import { settingIcons, settingTemplate } from './setting-template'
|
||||
import './setting-style'
|
||||
|
||||
export default class Setting {
|
||||
constructor(art, danmuku) {
|
||||
@@ -20,7 +12,6 @@ export default class Setting {
|
||||
|
||||
const { setStyle } = this.utils
|
||||
const { $controlsCenter } = art.template
|
||||
setStyle($controlsCenter, 'display', 'flex')
|
||||
|
||||
this.template = {
|
||||
$controlsCenter,
|
||||
@@ -57,46 +48,59 @@ export default class Setting {
|
||||
this.isLock = false
|
||||
this.timer = null
|
||||
|
||||
this.createTemplate()
|
||||
this.createSliders()
|
||||
this.createEvents()
|
||||
this.lifecycle = new SettingLifecycle(art)
|
||||
this.destroy = this.destroy.bind(this)
|
||||
this.lifecycle.own(() => art.off('destroy', this.destroy))
|
||||
|
||||
this.mount(this.option.mount)
|
||||
try {
|
||||
art.on('destroy', this.destroy)
|
||||
if (!this.lifecycle.active)
|
||||
return
|
||||
this.lifecycle.write($controlsCenter.style, 'display', 'flex', value => setStyle($controlsCenter, 'display', value))
|
||||
|
||||
art.on('resize', () => this.resize())
|
||||
art.on('fullscreen', state => this.onFullscreen(state))
|
||||
art.on('fullscreenWeb', state => this.onFullscreen(state))
|
||||
this.createTemplate()
|
||||
if (!this.lifecycle.active)
|
||||
return
|
||||
this.createSliders()
|
||||
if (!this.lifecycle.active)
|
||||
return
|
||||
this.createEvents()
|
||||
|
||||
art.proxy(this.template.$config, 'mouseenter', () => {
|
||||
this.onMouseEnter({
|
||||
$control: this.template.$config,
|
||||
$panel: this.template.$configPanel,
|
||||
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,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
art.proxy(this.template.$style, 'mouseenter', () => {
|
||||
this.onMouseEnter({
|
||||
$control: this.template.$style,
|
||||
$panel: this.template.$stylePanel,
|
||||
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 {
|
||||
$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,
|
||||
}
|
||||
return settingIcons()
|
||||
}
|
||||
|
||||
get option() {
|
||||
@@ -108,100 +112,7 @@ export default class Setting {
|
||||
}
|
||||
|
||||
get TEMPLATE() {
|
||||
const { option } = this
|
||||
return `
|
||||
<div class="apd-toggle">
|
||||
${$on}${$off}
|
||||
</div>
|
||||
<div class="apd-config">
|
||||
${$config}
|
||||
<div class="apd-config-panel">
|
||||
<div class="apd-config-panel-inner">
|
||||
<div class="apd-config-mode">
|
||||
按类型屏蔽
|
||||
<div class="apd-modes">
|
||||
<div data-mode="0" class="apd-mode">
|
||||
${$mode_0_off}${$mode_0_on}
|
||||
<div>滚动</div>
|
||||
</div>
|
||||
<div data-mode="1" class="apd-mode">
|
||||
${$mode_1_off}${$mode_1_on}
|
||||
<div>顶部</div>
|
||||
</div>
|
||||
<div data-mode="2" class="apd-mode">
|
||||
${$mode_2_off}${$mode_2_on}
|
||||
<div>底部</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="apd-config-other">
|
||||
<div class="apd-other apd-anti-overlap">
|
||||
${$check_on}${$check_off}
|
||||
防止弹幕重叠
|
||||
</div>
|
||||
<div class="apd-other apd-sync-video">
|
||||
${$check_on}${$check_off}
|
||||
同步视频速度
|
||||
</div>
|
||||
</div>
|
||||
<div class="apd-config-slider apd-config-opacity">
|
||||
不透明度
|
||||
<div class="apd-slider"></div>
|
||||
<div class="apd-value">未知</div>
|
||||
</div>
|
||||
<div class="apd-config-slider apd-config-margin">
|
||||
显示区域
|
||||
<div class="apd-slider"></div>
|
||||
<div class="apd-value">未知</div>
|
||||
</div>
|
||||
<div class="apd-config-slider apd-config-fontSize">
|
||||
弹幕字号
|
||||
<div class="apd-slider"></div>
|
||||
<div class="apd-value">未知</div>
|
||||
</div>
|
||||
<div class="apd-config-slider apd-config-speed">
|
||||
弹幕速度
|
||||
<div class="apd-slider"></div>
|
||||
<div class="apd-value">未知</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="apd-emitter">
|
||||
<div class="apd-style">
|
||||
${$style}
|
||||
<div class="apd-style-panel">
|
||||
<div class="apd-style-panel-inner">
|
||||
<div class="apd-style-mode">
|
||||
模式
|
||||
<div class="apd-modes">
|
||||
<div data-mode="0" class="apd-mode">
|
||||
${$mode_0_on}
|
||||
<div>滚动</div>
|
||||
</div>
|
||||
<div data-mode="1" class="apd-mode">
|
||||
${$mode_1_on}
|
||||
<div>顶部</div>
|
||||
</div>
|
||||
<div data-mode="2" class="apd-mode">
|
||||
${$mode_2_on}
|
||||
<div>底部</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="apd-style-color">
|
||||
颜色
|
||||
<div class="apd-colors">
|
||||
${this.COLOR.map(color => `<div data-color="${color}" class="apd-color" style="background-color: ${color}"></div>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input class="apd-input" placeholder="发个友善的弹幕见证当下" autocomplete="off" maxLength="${option.maxLength}" />
|
||||
<div class="apd-send">发送</div>
|
||||
</div>
|
||||
`
|
||||
return settingTemplate.call(this)
|
||||
}
|
||||
|
||||
get OPACITY() {
|
||||
@@ -308,6 +219,8 @@ export default class Setting {
|
||||
}
|
||||
|
||||
append(el, target) {
|
||||
if (!this.lifecycle.active)
|
||||
return
|
||||
const { append } = this.utils
|
||||
const children = [...el.children]
|
||||
if (children.includes(target))
|
||||
@@ -316,11 +229,13 @@ export default class Setting {
|
||||
}
|
||||
|
||||
setData(key, value) {
|
||||
if (!this.lifecycle.active)
|
||||
return
|
||||
const { $player } = this.art.template
|
||||
const { $mount } = this.template
|
||||
$player.dataset[key] = value
|
||||
this.lifecycle.write($player.dataset, key, value)
|
||||
if (this.outside) {
|
||||
$mount.dataset[key] = value
|
||||
this.lifecycle.write($mount.dataset, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,10 +243,10 @@ export default class Setting {
|
||||
const { createElement, tooltip } = this.utils
|
||||
|
||||
const $danmuku = createElement('div')
|
||||
this.template.$danmuku = $danmuku
|
||||
$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')
|
||||
@@ -355,11 +270,11 @@ export default class Setting {
|
||||
|
||||
const { $toggle } = this.template
|
||||
|
||||
this.art.on('artplayerPluginDanmuku:show', () => {
|
||||
this.lifecycle.on('artplayerPluginDanmuku:show', () => {
|
||||
tooltip($toggle, '关闭弹幕')
|
||||
})
|
||||
|
||||
this.art.on('artplayerPluginDanmuku:hide', () => {
|
||||
this.lifecycle.on('artplayerPluginDanmuku:hide', () => {
|
||||
tooltip($toggle, '打开弹幕')
|
||||
})
|
||||
}
|
||||
@@ -367,14 +282,14 @@ export default class Setting {
|
||||
createEvents() {
|
||||
const { $toggle, $configModes, $styleModes, $colors, $antiOverlap, $syncVideo, $send, $input } = this.template
|
||||
|
||||
this.art.proxy($toggle, 'click', () => {
|
||||
this.lifecycle.proxy($toggle, 'click', () => {
|
||||
this.danmuku.config({
|
||||
visible: !this.option.visible,
|
||||
})
|
||||
this.reset()
|
||||
})
|
||||
|
||||
this.art.proxy($configModes, 'click', (event) => {
|
||||
this.lifecycle.proxy($configModes, 'click', (event) => {
|
||||
const $mode = event.target.closest('.apd-mode')
|
||||
if (!$mode)
|
||||
return
|
||||
@@ -392,21 +307,21 @@ export default class Setting {
|
||||
this.reset()
|
||||
})
|
||||
|
||||
this.art.proxy($antiOverlap, 'click', () => {
|
||||
this.lifecycle.proxy($antiOverlap, 'click', () => {
|
||||
this.danmuku.config({
|
||||
antiOverlap: !this.option.antiOverlap,
|
||||
})
|
||||
this.reset()
|
||||
})
|
||||
|
||||
this.art.proxy($syncVideo, 'click', () => {
|
||||
this.lifecycle.proxy($syncVideo, 'click', () => {
|
||||
this.danmuku.config({
|
||||
synchronousPlayback: !this.option.synchronousPlayback,
|
||||
})
|
||||
this.reset()
|
||||
})
|
||||
|
||||
this.art.proxy($styleModes, 'click', (event) => {
|
||||
this.lifecycle.proxy($styleModes, 'click', (event) => {
|
||||
const $mode = event.target.closest('.apd-mode')
|
||||
if (!$mode)
|
||||
return
|
||||
@@ -417,7 +332,7 @@ export default class Setting {
|
||||
this.reset()
|
||||
})
|
||||
|
||||
this.art.proxy($colors, 'click', (event) => {
|
||||
this.lifecycle.proxy($colors, 'click', (event) => {
|
||||
const $color = event.target.closest('.apd-color')
|
||||
if (!$color)
|
||||
return
|
||||
@@ -427,9 +342,9 @@ export default class Setting {
|
||||
this.reset()
|
||||
})
|
||||
|
||||
this.art.proxy($send, 'click', () => this.emit())
|
||||
this.lifecycle.proxy($send, 'click', () => this.emit())
|
||||
|
||||
this.art.proxy($input, 'keypress', (event) => {
|
||||
this.lifecycle.proxy($input, 'keypress', (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
this.emit()
|
||||
@@ -509,79 +424,13 @@ export default class Setting {
|
||||
})
|
||||
}
|
||||
|
||||
createSlider({ min, max, container, findIndex, onChange, steps = [] }) {
|
||||
const { query, clamp, setStyle } = this.utils
|
||||
|
||||
setStyle(container, 'touch-action', 'none')
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="apd-slider-line">
|
||||
<div class="apd-slider-points">
|
||||
${steps.map(() => `<div class="apd-slider-point"></div>`).join('')}
|
||||
</div>
|
||||
<div class="apd-slider-progress"></div>
|
||||
</div>
|
||||
<div class="apd-slider-dot"></div>
|
||||
<div class="apd-slider-steps">
|
||||
${steps.map(step => (step.hide ? '' : `<div class="apd-slider-step">${step.name}</div>`)).join('')}
|
||||
</div>
|
||||
`
|
||||
|
||||
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 }
|
||||
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) {
|
||||
@@ -597,6 +446,8 @@ export default class Setting {
|
||||
}
|
||||
|
||||
onMouseEnter({ $control, $panel }) {
|
||||
if (!this.lifecycle.active)
|
||||
return
|
||||
const { $player } = this.art.template
|
||||
const controlRect = $control.getBoundingClientRect()
|
||||
const panelRect = $panel.getBoundingClientRect()
|
||||
@@ -617,80 +468,21 @@ export default class Setting {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
emit() {
|
||||
return emitSetting(this)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
loop()
|
||||
lockSetting(this)
|
||||
}
|
||||
|
||||
unlock() {
|
||||
const { removeClass } = this.utils
|
||||
const { $send } = this.template
|
||||
clearTimeout(this.timer)
|
||||
this.isLock = false
|
||||
$send.textContent = '发送'
|
||||
removeClass($send, 'apd-lock')
|
||||
unlockSetting(this)
|
||||
}
|
||||
|
||||
resize() {
|
||||
if (!this.lifecycle.active)
|
||||
return
|
||||
if (this.outside)
|
||||
return
|
||||
if (this.art.fullscreen)
|
||||
@@ -708,13 +500,16 @@ export default class Setting {
|
||||
}
|
||||
|
||||
reset() {
|
||||
if (!this.lifecycle.active)
|
||||
return
|
||||
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()
|
||||
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)
|
||||
@@ -737,6 +532,8 @@ export default class Setting {
|
||||
}
|
||||
|
||||
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}`)
|
||||
@@ -744,22 +541,17 @@ export default class Setting {
|
||||
this.template.$mount = $el
|
||||
this.reset()
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
const id = 'artplayer-plugin-danmuku'
|
||||
let $style = document.getElementById(id)
|
||||
if (!$style) {
|
||||
$style = document.createElement('style')
|
||||
$style.id = id
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.head.appendChild($style)
|
||||
})
|
||||
}
|
||||
else {
|
||||
(document.head || document.documentElement).appendChild($style)
|
||||
}
|
||||
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)
|
||||
}
|
||||
$style.textContent = style
|
||||
}
|
||||
Reference in new issue
Block a user