mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-22 21:31:20 -08:00
feat: add danmuku plugin documentation and i18n setup guide - Created a new documentation file for the danmuku plugin with code examples. - Added a comprehensive guide for language settings (i18n) including default languages, importing languages, adding new languages, and modifying existing languages. - Included code snippets for initializing the Artplayer with different language settings. Add internationalization support for multiple languages in Artplayer - Introduced Arabic (ar), Czech (cs), Spanish (es), Persian (fa), French (fr), Indonesian (id), Polish (pl), Russian (ru), Turkish (tr), Vietnamese (vi), and Traditional Chinese (zh-tw) translations. - Created corresponding JavaScript and ES module files for each language containing translated strings for player controls and settings. - Added a new documentation file for language settings in the project. feat: add initial implementation of start option page with interactive code execution buttons feat: add danmuku plugin documentation and i18n setup guide - Created a new documentation file for the danmuku plugin, providing code examples for usage. - Added a comprehensive guide for language settings, including default languages, importing languages, adding new languages, and modifying existing languages. - Included code snippets for initializing the Artplayer with different language settings and importing language files. refactor: update danmuku worker implementation and remove unused i18n files - Changed the way the Danmuku class creates a Web Worker by using a worker loader instead of creating a blob. - Removed Arabic, Czech, Spanish, Persian, French, Indonesian, Polish, Russian, Turkish, Vietnamese, and Traditional Chinese i18n files as they are no longer needed. - Updated the Vite build configuration to disable banners for inline workers and added a plugin to remove worker banners from generated code. feat: add danmuku plugin and i18n setup documentation - Created a new documentation file for the danmuku plugin, providing code examples for usage. - Added a new documentation file for language settings (i18n), detailing how to set default languages, import languages, and add new languages. - Included code snippets for initializing the Artplayer with different language settings and examples for modifying existing languages. feat: add danmuku plugin and i18n setup documentation - Created a new documentation file for the danmuku plugin with code examples. - Added a new documentation file for language settings (i18n) including import instructions and examples for setting default and custom languages. - Updated the i18n documentation to include details on adding and modifying languages. feat: add danmuku plugin and i18n setup documentation - Created a new JavaScript file for the danmuku plugin documentation with multiple code execution buttons. - Added a new JavaScript file for i18n (internationalization) setup documentation, including instructions for language settings, importing languages, and modifying language options. - Included examples for setting default languages and adding new languages in the i18n documentation. Refactor code structure for improved readability and maintainability
732 lines
22 KiB
JavaScript
732 lines
22 KiB
JavaScript
import { bilibiliDanmuParseFromUrl } from './bilibili'
|
|
import DanmuWorker from './worker.js?worker&inline'
|
|
|
|
export default 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 = [] // 弹幕DOM节点池
|
|
this.isStop = false // 是否停止
|
|
this.isHide = false // 是否隐藏
|
|
this.timer = null // 定时器
|
|
this.index = 0 // 弹幕索引
|
|
|
|
// 格式化后的配置项
|
|
this.option = Danmuku.option
|
|
|
|
// 弹幕状态池
|
|
this.states = { wait: [], ready: [], emit: [], stop: [] }
|
|
|
|
// 初始化配置
|
|
this.config(option, true)
|
|
|
|
// 创建 Web Worker, 用于计算弹幕的 top 值
|
|
this.worker = new DanmuWorker()
|
|
|
|
// 绑定公用事件
|
|
this.start = this.start.bind(this)
|
|
this.stop = this.stop.bind(this)
|
|
this.reset = this.reset.bind(this)
|
|
this.resize = this.resize.bind(this)
|
|
this.destroy = this.destroy.bind(this)
|
|
|
|
// 监听事件
|
|
art.on('video:play', this.start)
|
|
art.on('video:playing', this.start)
|
|
art.on('video:pause', this.stop)
|
|
art.on('video:waiting', this.stop)
|
|
art.on('destroy', this.destroy)
|
|
art.on('resize', this.resize)
|
|
|
|
// 开始加载弹幕
|
|
this.load()
|
|
}
|
|
|
|
// 默认配置
|
|
static get option() {
|
|
return {
|
|
danmuku: [], // 弹幕数据
|
|
speed: 5, // 弹幕持续时间,范围在[1 ~ 10]
|
|
margin: [10, '25%'], // 弹幕上下边距,支持像素数字和百分比
|
|
opacity: 1, // 弹幕透明度,范围在[0 ~ 1]
|
|
color: '#FFFFFF', // 默认弹幕颜色,可以被单独弹幕项覆盖
|
|
mode: 0, // 默认弹幕模式: 0: 滚动,1: 顶部,2: 底部
|
|
modes: [0, 1, 2], // 弹幕可见的模式
|
|
fontSize: 25, // 弹幕字体大小,支持像素数字和百分比
|
|
antiOverlap: true, // 弹幕是否防重叠
|
|
synchronousPlayback: false, // 是否同步播放速度
|
|
mount: undefined, // 弹幕发射器挂载点, 默认为播放器控制栏中部
|
|
heatmap: false, // 是否开启热力图
|
|
width: 512, // 当播放器宽度小于此值时,弹幕发射器置于播放器底部
|
|
points: [], // 热力图数据
|
|
filter: () => true, // 弹幕载入前的过滤器,只支持返回布尔值
|
|
beforeEmit: () => true, // 弹幕发送前的过滤器,支持返回 Promise
|
|
beforeVisible: () => true, // 弹幕显示前的过滤器,支持返回 Promise
|
|
visible: true, // 弹幕层是否可见
|
|
emitter: true, // 是否开启弹幕发射器
|
|
maxLength: 200, // 弹幕输入框最大长度, 范围在[1 ~ 1000]
|
|
lockTime: 5, // 输入框锁定时间,范围在[1 ~ 60]
|
|
theme: 'dark', // 弹幕主题,支持 dark 和 light,只在自定义挂载时生效
|
|
OPACITY: {}, // 不透明度配置项
|
|
FONT_SIZE: {}, // 弹幕字号配置项
|
|
MARGIN: {}, // 显示区域配置项
|
|
SPEED: {}, // 弹幕速度配置项
|
|
COLOR: [], // 颜色列表配置项
|
|
}
|
|
}
|
|
|
|
// 配置校验
|
|
static get scheme() {
|
|
return {
|
|
danmuku: 'array|function|string',
|
|
speed: 'number',
|
|
margin: 'array',
|
|
opacity: 'number',
|
|
color: 'string',
|
|
mode: 'number',
|
|
modes: 'array',
|
|
fontSize: 'number|string',
|
|
antiOverlap: 'boolean',
|
|
synchronousPlayback: 'boolean',
|
|
mount: '?htmldivelement|string',
|
|
heatmap: 'object|boolean',
|
|
width: 'number',
|
|
points: 'array',
|
|
filter: 'function',
|
|
beforeEmit: 'function',
|
|
beforeVisible: 'function',
|
|
visible: 'boolean',
|
|
emitter: 'boolean',
|
|
maxLength: 'number',
|
|
lockTime: 'number',
|
|
theme: 'string',
|
|
OPACITY: 'object',
|
|
FONT_SIZE: 'object',
|
|
MARGIN: 'object',
|
|
SPEED: 'object',
|
|
COLOR: 'array',
|
|
}
|
|
}
|
|
|
|
// 初始弹幕样式
|
|
static get cssText() {
|
|
return `
|
|
user-select: none;
|
|
position: absolute;
|
|
white-space: pre;
|
|
pointer-events: none;
|
|
perspective: 500px;
|
|
display: inline-block;
|
|
will-change: transform;
|
|
font-weight: normal;
|
|
line-height: 1.125;
|
|
visibility: hidden;
|
|
font-family: SimHei, "Microsoft JhengHei", Arial, Helvetica, sans-serif;
|
|
text-shadow: rgb(0, 0, 0) 1px 0px 1px, rgb(0, 0, 0) 0px 1px 1px, rgb(0, 0, 0) 0px -1px 1px, rgb(0, 0, 0) -1px 0px 1px;
|
|
`
|
|
}
|
|
|
|
// 是否在移动端使用了自动旋屏,会影响弹幕的left和top值
|
|
get isRotate() {
|
|
return this.art.plugins?.autoOrientation?.state
|
|
}
|
|
|
|
// 计算上空白边距
|
|
get marginTop() {
|
|
const { clamp } = this.utils
|
|
const value = this.option.margin[0]
|
|
const { clientHeight } = this.$player
|
|
|
|
if (typeof value === 'number') {
|
|
return clamp(value, 0, clientHeight)
|
|
}
|
|
|
|
if (typeof value === 'string' && value.endsWith('%')) {
|
|
const ratio = Number.parseFloat(value) / 100
|
|
return clamp(clientHeight * ratio, 0, clientHeight)
|
|
}
|
|
|
|
return Danmuku.option.margin[0]
|
|
}
|
|
|
|
// 计算下空白边距
|
|
get marginBottom() {
|
|
const { clamp } = this.utils
|
|
const value = this.option.margin[1]
|
|
const { clientHeight } = this.$player
|
|
|
|
if (typeof value === 'number') {
|
|
return clamp(value, 0, clientHeight)
|
|
}
|
|
|
|
if (typeof value === 'string' && value.endsWith('%')) {
|
|
const ratio = Number.parseFloat(value) / 100
|
|
return clamp(clientHeight * ratio, 0, clientHeight)
|
|
}
|
|
|
|
return Danmuku.option.margin[1]
|
|
}
|
|
|
|
// 计算弹幕字体大小
|
|
get fontSize() {
|
|
const { clamp } = this.utils
|
|
const { clientHeight } = this.$player
|
|
|
|
const fontSize = this.option.fontSize
|
|
|
|
if (typeof fontSize === 'number') {
|
|
return Math.round(clamp(fontSize, 12, clientHeight))
|
|
}
|
|
|
|
if (typeof fontSize === 'string' && fontSize.endsWith('%')) {
|
|
const ratio = Number.parseFloat(fontSize) / 100
|
|
return Math.round(clamp(clientHeight * ratio, 12, clientHeight))
|
|
}
|
|
|
|
return Danmuku.option.fontSize
|
|
}
|
|
|
|
// 获取弹幕DOM节点
|
|
get $ref() {
|
|
const $ref = this.$refs.pop() || document.createElement('div')
|
|
$ref.style.cssText = Danmuku.cssText
|
|
$ref.dataset.mode = ''
|
|
$ref.dataset.id = ''
|
|
$ref.className = ''
|
|
return $ref
|
|
}
|
|
|
|
// 获取准备好发送的弹幕
|
|
get readys() {
|
|
const { currentTime } = this.art
|
|
|
|
const result = []
|
|
|
|
// 有的是ready状态:之前因为弹幕太多而暂停发送的弹幕
|
|
this.filter('ready', danmu => result.push(danmu))
|
|
|
|
// 有的是wait状态:符合时间范围的弹幕
|
|
this.filter('wait', (danmu) => {
|
|
if (currentTime + 0.1 >= danmu.time && danmu.time >= currentTime - 0.1) {
|
|
result.push(danmu)
|
|
}
|
|
})
|
|
|
|
return result
|
|
}
|
|
|
|
// 可见的弹幕的数据,用于计算下一个弹幕的top值
|
|
get visibles() {
|
|
const result = []
|
|
const { clientWidth } = this.$player
|
|
const clientLeft = this.getLeft(this.$player)
|
|
|
|
this.filter('emit', (danmu) => {
|
|
const top = danmu.$ref.offsetTop
|
|
const left = this.getLeft(danmu.$ref) - clientLeft
|
|
const height = danmu.$ref.clientHeight
|
|
const width = danmu.$ref.clientWidth
|
|
const distance = left + width
|
|
const right = clientWidth - distance
|
|
const speed = distance / danmu.$restTime
|
|
|
|
const emit = {}
|
|
emit.top = top
|
|
emit.left = left
|
|
emit.height = height
|
|
emit.width = width
|
|
emit.right = right
|
|
emit.speed = speed
|
|
emit.distance = distance
|
|
emit.time = danmu.$restTime
|
|
emit.mode = danmu.mode
|
|
|
|
result.push(emit)
|
|
})
|
|
|
|
return result
|
|
}
|
|
|
|
// 计算弹幕速度
|
|
get speed() {
|
|
return this.option.synchronousPlayback && this.art.playbackRate
|
|
? this.option.speed / Number(this.art.playbackRate)
|
|
: this.option.speed
|
|
}
|
|
|
|
// 加载弹幕
|
|
async load(danmuku) {
|
|
const { errorHandle } = this.utils
|
|
|
|
let danmus = []
|
|
const target = danmuku || this.option.danmuku
|
|
|
|
try {
|
|
if (typeof target === 'function') {
|
|
danmus = await target() // 异步函数获取
|
|
}
|
|
else if (target instanceof Promise) {
|
|
danmus = await target // 从 Promise 对象获取
|
|
}
|
|
else if (typeof target === 'string') {
|
|
danmus = await bilibiliDanmuParseFromUrl(target) // 从B站xml链接解析
|
|
}
|
|
else if (Array.isArray(target)) {
|
|
danmus = target // 直接传入数组
|
|
}
|
|
|
|
errorHandle(Array.isArray(danmus), 'Danmuku need return an array as result')
|
|
|
|
// 假如没有传入弹幕参数,则清空弹幕,否则追加弹幕
|
|
if (danmuku === undefined) {
|
|
this.reset() // 重置弹幕
|
|
this.queue = [] // 清空弹幕队列
|
|
this.states = { wait: [], ready: [], emit: [], stop: [] } // 清空弹幕状态池
|
|
this.$refs = [] // 清空弹幕DOM节点池
|
|
this.$danmuku.textContent = '' // 清空弹幕层
|
|
}
|
|
|
|
// 逐个验证原始弹幕并转换到弹幕队列
|
|
for (let index = 0; index < danmus.length; index++) {
|
|
const danmu = danmus[index]
|
|
await this.emit(danmu)
|
|
}
|
|
|
|
this.art.emit('artplayerPluginDanmuku:loaded', this.queue)
|
|
}
|
|
catch (error) {
|
|
this.art.emit('artplayerPluginDanmuku:error', error)
|
|
throw error
|
|
}
|
|
|
|
return this
|
|
}
|
|
|
|
// 把原始弹幕转换到弹幕队列
|
|
async emit(danmu) {
|
|
const { clamp } = this.utils
|
|
|
|
this.validator(danmu, {
|
|
id: '?string', // 弹幕唯一标识
|
|
text: 'string', // 弹幕文本
|
|
mode: '?number', // 弹幕模式: 0: 滚动,1: 顶部,2: 底部
|
|
color: '?string', // 弹幕颜色
|
|
time: '?number', // 弹幕时间
|
|
border: '?boolean', // 弹幕是否有边框
|
|
style: '?object', // 弹幕额外样式
|
|
})
|
|
|
|
// 弹幕文本为空则直接忽略
|
|
if (!danmu.text.trim())
|
|
return this
|
|
|
|
// 设置弹幕时间,如果没有则默认为当前时间加 0.5 秒
|
|
if (danmu.time) {
|
|
danmu.time = clamp(danmu.time, 0, Infinity)
|
|
}
|
|
else {
|
|
danmu.time = this.art.currentTime + 0.5
|
|
}
|
|
|
|
// 设置弹幕模式,如果没有则默认为全局配置
|
|
if (danmu.mode === undefined) {
|
|
danmu.mode = this.option.mode
|
|
}
|
|
|
|
// 设置弹幕单独样式,如果没有则默认为空对象
|
|
if (danmu.style === undefined) {
|
|
danmu.style = {}
|
|
}
|
|
|
|
// 设置弹幕颜色,如果没有则默认为全局配置
|
|
if (danmu.color === undefined) {
|
|
danmu.color = this.option.color
|
|
}
|
|
|
|
// 弹幕模式只能是 0, 1, 2
|
|
if (![0, 1, 2].includes(danmu.mode))
|
|
return this
|
|
|
|
// 自定义弹幕过滤函数
|
|
if (!this.option.filter(danmu))
|
|
return this
|
|
|
|
// 添加自定义属性
|
|
const item = {
|
|
...danmu,
|
|
$state: 'wait', // 弹幕初始状态
|
|
$index: this.index++, // 弹幕索引
|
|
$ref: null, // 弹幕 DOM 节点
|
|
$restTime: 0, // 弹幕剩余时间
|
|
$lastStartTime: 0, // 弹幕上次开始时间
|
|
}
|
|
|
|
// 转换为wait状态
|
|
this.setState(item, 'wait')
|
|
|
|
// 添加到实际弹幕队列
|
|
this.queue.push(item)
|
|
|
|
// 弹幕有四个状态:
|
|
// - wait: 弹幕还未开始显示,没有被添加到 DOM 中
|
|
// - ready: 弹幕准备好显示,没有被添加到 DOM 中
|
|
// - emit: 弹幕正在显示,已经被添加到 DOM 中
|
|
// - stop: 弹幕正在停止显示,已经被添加到 DOM 中
|
|
|
|
return this
|
|
}
|
|
|
|
// 动态配置
|
|
config(option, isInit = false) {
|
|
const { clamp } = this.utils
|
|
const { $controlsCenter } = this.art.template
|
|
|
|
// 判断配置项是否有变化
|
|
const changed = Object.keys(option).some(
|
|
key => JSON.stringify(this.option[key]) !== JSON.stringify(option[key]),
|
|
)
|
|
|
|
// 没有变化则直接返回
|
|
if (!changed && !isInit)
|
|
return this
|
|
|
|
// 更新配置项
|
|
this.option = Object.assign({}, Danmuku.option, this.option, option)
|
|
this.validator(this.option, Danmuku.scheme)
|
|
|
|
this.option.mode = clamp(this.option.mode, 0, 2)
|
|
this.option.speed = clamp(this.option.speed, 1, 10)
|
|
this.option.opacity = clamp(this.option.opacity, 0, 1)
|
|
this.option.lockTime = clamp(this.option.lockTime, 1, 60)
|
|
this.option.maxLength = clamp(this.option.maxLength, 1, 1000)
|
|
this.option.mount = this.option.mount || $controlsCenter
|
|
|
|
// 动态配置有字体大小,需要重新渲染
|
|
if (option.fontSize) {
|
|
this.reset()
|
|
}
|
|
|
|
// 通过配置项控制弹幕的显示和隐藏
|
|
if (this.option.visible) {
|
|
this.show()
|
|
}
|
|
else {
|
|
this.hide()
|
|
}
|
|
|
|
this.art.emit('artplayerPluginDanmuku:config', this.option)
|
|
|
|
return this
|
|
}
|
|
|
|
// 计算DOM的left值,受到旋屏影响
|
|
getLeft($ref) {
|
|
const rect = $ref.getBoundingClientRect()
|
|
return this.isRotate ? rect.top : rect.left
|
|
}
|
|
|
|
// 复杂运算交给 Web Worker 处理
|
|
postMessage(message = {}) {
|
|
return new Promise((resolve) => {
|
|
message.id = Date.now() // 生成唯一标识
|
|
this.worker.postMessage(message)
|
|
this.worker.onmessage = (event) => {
|
|
const { data } = event
|
|
// 判断是否是当前的消息
|
|
if (data.id === message.id) {
|
|
resolve(data)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// 根据状态获取弹幕
|
|
filter(state, callback) {
|
|
const danmus = this.states[state] || []
|
|
for (let index = 0; index < danmus.length; index++) {
|
|
callback(danmus[index])
|
|
}
|
|
return danmus
|
|
}
|
|
|
|
// 设置弹幕状态
|
|
setState(danmu, state) {
|
|
// 从原状态池中删除
|
|
this.states[danmu.$state] = this.states[danmu.$state].filter(item => item !== danmu)
|
|
|
|
// 设置新状态
|
|
danmu.$state = state
|
|
|
|
// 设置DOM节点状态
|
|
if (danmu.$ref) {
|
|
danmu.$ref.dataset.state = state
|
|
}
|
|
|
|
// 添加到新状态池中
|
|
this.states[state].push(danmu)
|
|
}
|
|
|
|
// 重置弹幕到wait状态,回收弹幕DOM节点
|
|
makeWait(danmu) {
|
|
this.setState(danmu, 'wait')
|
|
if (danmu.$ref) {
|
|
danmu.$ref.style.cssText = Danmuku.cssText
|
|
danmu.$ref.style.visibility = 'hidden'
|
|
danmu.$ref.style.marginLeft = '0px'
|
|
danmu.$ref.style.transform = 'translateX(0px)'
|
|
danmu.$ref.style.transition = 'transform 0s linear 0s'
|
|
this.$refs.push(danmu.$ref)
|
|
danmu.$ref = null
|
|
}
|
|
}
|
|
|
|
// 实时更新弹幕
|
|
update() {
|
|
const { setStyles } = this.utils
|
|
|
|
this.timer = window.requestAnimationFrame(async () => {
|
|
if (this.art.playing && !this.isHide) {
|
|
// 实时计算弹幕的剩余显示时间
|
|
this.filter('emit', (danmu) => {
|
|
const emitTime = (Date.now() - danmu.$lastStartTime) / 1000
|
|
danmu.$restTime -= emitTime
|
|
danmu.$lastStartTime = Date.now()
|
|
// 超过时间即重置弹幕
|
|
if (danmu.$restTime <= 0) {
|
|
this.makeWait(danmu)
|
|
}
|
|
})
|
|
|
|
// 获取准备好发送的弹幕,可能包含ready和wait状态的弹幕
|
|
const readys = this.readys
|
|
|
|
for (let index = 0; index < readys.length; index++) {
|
|
const danmu = readys[index]
|
|
|
|
// 弹幕发送前的过滤器
|
|
const state = await this.option.beforeVisible(danmu)
|
|
|
|
if (state) {
|
|
const { clientWidth, clientHeight } = this.$player
|
|
danmu.$ref = this.$ref // 获取弹幕DOM节点
|
|
danmu.$ref.textContent = danmu.text // 设置弹幕文本
|
|
|
|
// 提前添加到弹幕层中,用于计算top值
|
|
this.$danmuku.appendChild(danmu.$ref)
|
|
|
|
// 设置初始弹幕样式
|
|
danmu.$ref.style.opacity = this.option.opacity
|
|
danmu.$ref.style.fontSize = `${this.fontSize}px`
|
|
danmu.$ref.style.color = danmu.color
|
|
danmu.$ref.style.border = danmu.border ? `1px solid ${danmu.color}` : null
|
|
danmu.$ref.style.backgroundColor = danmu.border ? 'rgb(0 0 0 / 50%)' : null
|
|
|
|
// 设置单独弹幕样式
|
|
setStyles(danmu.$ref, danmu.style)
|
|
|
|
// 记录弹幕时间戳
|
|
danmu.$lastStartTime = Date.now()
|
|
|
|
// 计算弹幕剩余时间
|
|
danmu.$restTime = this.speed
|
|
|
|
// 计算弹幕滚动的距离
|
|
const distance = clientWidth + danmu.$ref.clientWidth
|
|
|
|
// 计算弹幕的top值
|
|
const { result: top } = await this.postMessage({
|
|
type: 'getDanmuTop',
|
|
target: {
|
|
mode: danmu.mode,
|
|
height: danmu.$ref.clientHeight,
|
|
speed: distance / danmu.$restTime,
|
|
}, // 当前弹幕信息
|
|
visibles: this.visibles, // 可见的弹幕的数据
|
|
antiOverlap: this.option.antiOverlap,
|
|
clientWidth,
|
|
clientHeight,
|
|
marginBottom: this.marginBottom,
|
|
marginTop: this.marginTop,
|
|
})
|
|
|
|
if (danmu.$ref) {
|
|
if (!this.isStop && top !== undefined) {
|
|
this.setState(danmu, 'emit') // 转换为emit状态
|
|
danmu.$ref.style.top = `${top}px`
|
|
danmu.$ref.style.visibility = 'visible'
|
|
danmu.$ref.dataset.mode = danmu.mode // CSS控制模式的显示和隐藏
|
|
danmu.$ref.dataset.id = danmu.id || '' // 用于悬停的唯一标识
|
|
|
|
switch (danmu.mode) {
|
|
// 滚动的弹幕
|
|
case 0: {
|
|
danmu.$ref.style.left = `${clientWidth}px`
|
|
danmu.$ref.style.marginLeft = '0px'
|
|
danmu.$ref.style.transform = `translateX(${-distance}px)`
|
|
danmu.$ref.style.transition = `transform ${danmu.$restTime}s linear 0s`
|
|
break
|
|
}
|
|
case 1:
|
|
// falls through
|
|
case 2:
|
|
danmu.$ref.style.left = '50%'
|
|
danmu.$ref.style.marginLeft = `-${danmu.$ref.clientWidth / 2}px`
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
|
|
this.art.emit('artplayerPluginDanmuku:visible', danmu)
|
|
}
|
|
else {
|
|
// 假如弹幕已经停止或者没有 top 值,则重置弹幕为ready状态,回收弹幕DOM节点,等待下次发送
|
|
this.setState(danmu, 'ready')
|
|
this.$refs.push(danmu.$ref)
|
|
danmu.$ref = null
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 递归调用
|
|
if (!this.isStop) {
|
|
this.update()
|
|
}
|
|
})
|
|
return this
|
|
}
|
|
|
|
// 重置正在显示的弹幕: stop/emit 状态的弹幕
|
|
resize() {
|
|
const { clientWidth } = this.$player
|
|
|
|
this.filter('stop', (danmu) => {
|
|
switch (danmu.mode) {
|
|
// 滚动的弹幕
|
|
case 0:
|
|
danmu.$ref.style.left = `${clientWidth}px`
|
|
break
|
|
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
|
|
}
|
|
})
|
|
}
|
|
|
|
// 继续弹幕
|
|
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
|
|
}
|
|
})
|
|
|
|
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
|
|
}
|
|
})
|
|
|
|
return this
|
|
}
|
|
|
|
stop() {
|
|
this.isStop = true
|
|
this.suspend()
|
|
window.cancelAnimationFrame(this.timer)
|
|
this.art.emit('artplayerPluginDanmuku:stop')
|
|
return this
|
|
}
|
|
|
|
start() {
|
|
this.isStop = false
|
|
this.continue()
|
|
this.update()
|
|
this.art.emit('artplayerPluginDanmuku:start')
|
|
return this
|
|
}
|
|
|
|
reset() {
|
|
this.queue.forEach(danmu => this.makeWait(danmu))
|
|
this.art.emit('artplayerPluginDanmuku:reset')
|
|
return this
|
|
}
|
|
|
|
show() {
|
|
this.isHide = false
|
|
this.$danmuku.style.opacity = 1
|
|
this.option.visible = true
|
|
this.art.emit('artplayerPluginDanmuku:show')
|
|
return this
|
|
}
|
|
|
|
hide() {
|
|
this.isHide = true
|
|
this.$danmuku.style.opacity = 0
|
|
this.option.visible = false
|
|
this.art.emit('artplayerPluginDanmuku:hide')
|
|
return this
|
|
}
|
|
|
|
destroy() {
|
|
this.stop()
|
|
this.worker.terminate()
|
|
this.art.off('video:play', this.start)
|
|
this.art.off('video:playing', this.start)
|
|
this.art.off('video:pause', this.stop)
|
|
this.art.off('video:waiting', this.stop)
|
|
this.art.off('resize', this.reset)
|
|
this.art.off('destroy', this.destroy)
|
|
this.art.emit('artplayerPluginDanmuku:destroy')
|
|
}
|
|
}
|