mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-28 10:31:20 -08:00
refactor(danmuku): [PKG-DANMUKU-06] migrate runtime and preserve public types
This commit is contained in:
1 parent
abc8008da7
commit
24b883d862
64 files changed
+14020
-633
No files matched your search
+10
-8
@@ -1,4 +1,6 @@
|
||||
export function getMode(key) {
|
||||
import type { ParsedDanmu, ParserReply, ParserRequest } from './parser-types'
|
||||
|
||||
export function getMode(key: number): number {
|
||||
switch (key) {
|
||||
case 1:
|
||||
case 2:
|
||||
@@ -13,16 +15,16 @@ export function getMode(key) {
|
||||
}
|
||||
}
|
||||
|
||||
export function bilibiliDanmuParseFromXml(xmlString) {
|
||||
export function bilibiliDanmuParseFromXml(xmlString: unknown): ParsedDanmu[] {
|
||||
if (typeof xmlString !== 'string')
|
||||
return []
|
||||
const reg = /<d[^>]*?p="(?<p>[^"]+)"[^>]*>(?<text>.*?)<\/d>/gs
|
||||
const matches = xmlString.matchAll(reg)
|
||||
return Array.from(matches)
|
||||
.map((match) => {
|
||||
const attr = match.groups.p.split(',')
|
||||
const attr = match.groups!.p!.split(',')
|
||||
if (attr.length >= 8) {
|
||||
const text = match.groups.text
|
||||
const text = match.groups!.text!
|
||||
.trim()
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll(''', '\'')
|
||||
@@ -38,7 +40,7 @@ export function bilibiliDanmuParseFromXml(xmlString) {
|
||||
color: `#${Number(attr[3]).toString(16)}`,
|
||||
timestamp: Number(attr[4]),
|
||||
pool: Number(attr[5]),
|
||||
userID: attr[6],
|
||||
userID: attr[6]!,
|
||||
rowID: Number(attr[7]),
|
||||
}
|
||||
}
|
||||
@@ -46,11 +48,11 @@ export function bilibiliDanmuParseFromXml(xmlString) {
|
||||
return null
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
.filter(Boolean) as ParsedDanmu[]
|
||||
}
|
||||
|
||||
export function onmessage({ data }) {
|
||||
export function onmessage({ data }: MessageEvent<ParserRequest>) {
|
||||
const { xml, id } = data
|
||||
const danmus = bilibiliDanmuParseFromXml(xml)
|
||||
globalThis.postMessage({ danmus, id })
|
||||
globalThis.postMessage({ danmus, id } satisfies ParserReply)
|
||||
}
|
||||
+15
-12
@@ -1,8 +1,9 @@
|
||||
import type { ParsedDanmu, ParserCancellation, ParserReply, ParserRequest } from './parser-types'
|
||||
import { bilibiliDanmuParseFromXml, getMode, onmessage } from './bilibili-parser'
|
||||
|
||||
let nextRequest = 0
|
||||
|
||||
function abortError(signal) {
|
||||
function abortError(signal?: AbortSignal): unknown {
|
||||
if (signal?.reason !== undefined)
|
||||
return signal.reason
|
||||
const error = new Error('Bilibili Danmu loading cancelled')
|
||||
@@ -10,17 +11,17 @@ function abortError(signal) {
|
||||
return error
|
||||
}
|
||||
|
||||
export function bilibiliDanmuParseFromUrl(url, { signal, onCancel } = {}) {
|
||||
export function bilibiliDanmuParseFromUrl(url: string, { signal, onCancel }: ParserCancellation = {}): Promise<ParsedDanmu[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let settled = false
|
||||
let worker
|
||||
let workerUrl
|
||||
let releaseCancel
|
||||
let worker: Worker | undefined
|
||||
let workerUrl: string | undefined
|
||||
let releaseCancel: (() => void) | undefined
|
||||
const id = ++nextRequest
|
||||
const cancel = () => finish(undefined, abortError(signal))
|
||||
|
||||
function releaseWorker() {
|
||||
let error
|
||||
let error: unknown
|
||||
let failed = false
|
||||
try {
|
||||
if (worker) {
|
||||
@@ -52,7 +53,9 @@ export function bilibiliDanmuParseFromUrl(url, { signal, onCancel } = {}) {
|
||||
throw error
|
||||
}
|
||||
|
||||
function finish(value, error) {
|
||||
function finish(value: ParsedDanmu[]): void
|
||||
function finish(value: undefined, error: unknown): void
|
||||
function finish(value: ParsedDanmu[] | undefined, error?: unknown) {
|
||||
if (settled)
|
||||
return
|
||||
settled = true
|
||||
@@ -69,10 +72,10 @@ export function bilibiliDanmuParseFromUrl(url, { signal, onCancel } = {}) {
|
||||
}
|
||||
if (failed)
|
||||
reject(error)
|
||||
else resolve(value)
|
||||
else resolve(value!)
|
||||
}
|
||||
|
||||
function fallback(xml, error) {
|
||||
function fallback(xml: string, error: unknown) {
|
||||
if (settled)
|
||||
return
|
||||
console.error('Error parsing Bilibili Danmu:', error)
|
||||
@@ -107,7 +110,7 @@ export function bilibiliDanmuParseFromUrl(url, { signal, onCancel } = {}) {
|
||||
releaseWorker()
|
||||
return
|
||||
}
|
||||
worker.onmessage = (event) => {
|
||||
worker.onmessage = (event: MessageEvent<ParserReply>) => {
|
||||
if (settled || event.data?.id !== id)
|
||||
return
|
||||
const { danmus } = event.data
|
||||
@@ -121,8 +124,8 @@ export function bilibiliDanmuParseFromUrl(url, { signal, onCancel } = {}) {
|
||||
event.preventDefault?.()
|
||||
fallback(xml, event.error || event)
|
||||
}
|
||||
worker.onmessageerror = event => fallback(xml, event.error || event)
|
||||
worker.postMessage({ xml, id })
|
||||
worker.onmessageerror = event => fallback(xml, (event as MessageEvent & { error?: unknown }).error || event)
|
||||
worker.postMessage({ xml, id } satisfies ParserRequest)
|
||||
}
|
||||
catch (error) {
|
||||
fallback(xml, error)
|
||||
+8
-6
@@ -1,4 +1,6 @@
|
||||
export function defaultOption() {
|
||||
import type { DanmukuOption, NormalizedOption } from './types'
|
||||
|
||||
export function defaultOption(): NormalizedOption & { fontSize: number, margin: [number, string] } {
|
||||
return {
|
||||
danmuku: [],
|
||||
speed: 5,
|
||||
@@ -62,21 +64,21 @@ export function optionScheme() {
|
||||
}
|
||||
}
|
||||
|
||||
export function isPromiseInput(value) {
|
||||
export function isPromiseInput(value: unknown): value is Promise<unknown> {
|
||||
return value instanceof Promise || Object.prototype.toString.call(value) === '[object Promise]'
|
||||
}
|
||||
|
||||
export function optionChanged(previous, update) {
|
||||
export function optionChanged(previous: NormalizedOption, update: DanmukuOption) {
|
||||
return Object.keys(update).some((key) => {
|
||||
const before = previous[key]
|
||||
const after = update[key]
|
||||
const before = previous[key as keyof NormalizedOption]
|
||||
const after = update[key as keyof NormalizedOption]
|
||||
if (typeof before === 'function' || typeof after === 'function' || isPromiseInput(before) || isPromiseInput(after))
|
||||
return before !== after
|
||||
return JSON.stringify(before) !== JSON.stringify(after)
|
||||
})
|
||||
}
|
||||
|
||||
export function normalizeOption(previous, update, { defaults, validate, clamp, mount }) {
|
||||
export function normalizeOption(previous: NormalizedOption, update: DanmukuOption, { defaults, validate, clamp, mount }: { defaults: NormalizedOption, validate: (value: NormalizedOption) => unknown, clamp: (value: number, min: number, max: number) => number, mount: HTMLDivElement }): NormalizedOption {
|
||||
const next = Object.assign({}, defaults, previous, update)
|
||||
validate(next)
|
||||
next.mode = clamp(next.mode, 0, 2)
|
||||
+46
-21
@@ -1,13 +1,34 @@
|
||||
import type { DanmuInput, DanmuItem, DanmukuArt, DanmukuInput, DanmukuOption, DanmuState, NormalizedDanmu, NormalizedOption } from './types'
|
||||
|
||||
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'
|
||||
import DanmuWorker from './worker.ts?worker&inline'
|
||||
|
||||
export default class Danmuku {
|
||||
constructor(art, option) {
|
||||
declare ['constructor']: typeof Danmuku
|
||||
declare art: DanmukuArt
|
||||
declare utils: DanmukuArt['constructor']['utils']
|
||||
declare validator: DanmukuArt['constructor']['validator']
|
||||
declare $danmuku: HTMLDivElement
|
||||
declare $player: HTMLDivElement
|
||||
declare queue: DanmuItem[]
|
||||
declare $refs: HTMLDivElement[]
|
||||
declare isStop: boolean
|
||||
declare isHide: boolean
|
||||
declare timer: number | null
|
||||
declare index: number
|
||||
declare worker: Worker | null
|
||||
declare workerClient: WorkerClient | null
|
||||
declare renderer: Renderer
|
||||
declare scheduler: Scheduler
|
||||
declare option: NormalizedOption
|
||||
declare states: Record<DanmuState, DanmuItem[]>
|
||||
|
||||
constructor(art: DanmukuArt, option: DanmukuOption) {
|
||||
const { constructor, template } = art
|
||||
|
||||
this.utils = constructor.utils // 工具库
|
||||
@@ -99,8 +120,8 @@ export default class Danmuku {
|
||||
}
|
||||
|
||||
// 是否在移动端使用了自动旋屏,会影响弹幕的left和top值
|
||||
get isRotate() {
|
||||
return this.art.plugins?.autoOrientation?.state
|
||||
get isRotate(): boolean | undefined {
|
||||
return (this.art.plugins?.autoOrientation as { state?: boolean } | undefined)?.state
|
||||
}
|
||||
|
||||
// 计算上空白边距
|
||||
@@ -181,7 +202,7 @@ export default class Danmuku {
|
||||
}
|
||||
|
||||
// 加载弹幕
|
||||
async load(danmuku) {
|
||||
async load(danmuku?: DanmukuInput) {
|
||||
const { errorHandle } = this.utils
|
||||
const task = beginInput(this, danmuku === undefined)
|
||||
|
||||
@@ -196,6 +217,8 @@ export default class Danmuku {
|
||||
return this
|
||||
|
||||
errorHandle(Array.isArray(danmus), 'Danmuku need return an array as result')
|
||||
// Active input and the runtime validator exclude the cancellation sentinel.
|
||||
const rows = danmus as DanmuInput[]
|
||||
|
||||
// 假如没有传入弹幕参数,则清空弹幕,否则追加弹幕
|
||||
if (danmuku === undefined) {
|
||||
@@ -208,10 +231,10 @@ export default class Danmuku {
|
||||
}
|
||||
|
||||
// 逐个验证原始弹幕并转换到弹幕队列
|
||||
for (let index = 0; index < danmus.length; index++) {
|
||||
for (let index = 0; index < rows.length; index++) {
|
||||
if (!task.active())
|
||||
return this
|
||||
const danmu = danmus[index]
|
||||
const danmu = rows[index]!
|
||||
await task.emit(danmu)
|
||||
}
|
||||
|
||||
@@ -232,7 +255,7 @@ export default class Danmuku {
|
||||
}
|
||||
|
||||
// 把原始弹幕转换到弹幕队列
|
||||
async emit(danmu) {
|
||||
async emit(danmu: DanmuInput) {
|
||||
const { clamp } = this.utils
|
||||
if (!inputActive(this))
|
||||
return this
|
||||
@@ -279,7 +302,8 @@ export default class Danmuku {
|
||||
return this
|
||||
|
||||
// 自定义弹幕过滤函数
|
||||
if (!this.option.filter(danmu))
|
||||
// Validation/defaulting above fills these fields before the public callback.
|
||||
if (!this.option.filter(danmu as NormalizedDanmu))
|
||||
return this
|
||||
if (!inputActive(this))
|
||||
return this
|
||||
@@ -287,7 +311,7 @@ export default class Danmuku {
|
||||
// 添加自定义属性
|
||||
const item = {
|
||||
...danmu,
|
||||
$state: 'wait', // 弹幕初始状态
|
||||
$state: 'wait' as const, // 弹幕初始状态
|
||||
$index: this.index++, // 弹幕索引
|
||||
$ref: null, // 弹幕 DOM 节点
|
||||
$restTime: 0, // 弹幕剩余时间
|
||||
@@ -295,10 +319,10 @@ export default class Danmuku {
|
||||
}
|
||||
|
||||
// 转换为wait状态
|
||||
this.setState(item, 'wait')
|
||||
this.setState(item as DanmuItem, 'wait')
|
||||
|
||||
// 添加到实际弹幕队列
|
||||
this.queue.push(item)
|
||||
this.queue.push(item as DanmuItem)
|
||||
|
||||
// 弹幕有四个状态:
|
||||
// - wait: 弹幕还未开始显示,没有被添加到 DOM 中
|
||||
@@ -310,7 +334,7 @@ export default class Danmuku {
|
||||
}
|
||||
|
||||
// 动态配置
|
||||
config(option, isInit = false) {
|
||||
config(option: DanmukuOption, isInit = false) {
|
||||
const { clamp } = this.utils
|
||||
const { $controlsCenter } = this.art.template
|
||||
|
||||
@@ -359,13 +383,14 @@ export default class Danmuku {
|
||||
}
|
||||
|
||||
// 计算DOM的left值,受到旋屏影响
|
||||
getLeft($ref) {
|
||||
getLeft($ref: HTMLElement): number {
|
||||
return this.renderer.left($ref)
|
||||
}
|
||||
|
||||
// 复杂运算交给 Web Worker 处理
|
||||
postMessage(message = {}) {
|
||||
return this.workerClient.request(message)
|
||||
// Keep the historical empty default; unknown message types receive no reply.
|
||||
postMessage(message: Parameters<WorkerClient['request']>[0] = {} as Parameters<WorkerClient['request']>[0]) {
|
||||
return this.workerClient!.request(message)
|
||||
}
|
||||
|
||||
createWorker() {
|
||||
@@ -374,17 +399,17 @@ export default class Danmuku {
|
||||
}
|
||||
|
||||
// 根据状态获取弹幕
|
||||
filter(state, callback) {
|
||||
filter(state: DanmuState, callback: (danmu: DanmuItem) => void) {
|
||||
return filterState(this, state, callback)
|
||||
}
|
||||
|
||||
// 设置弹幕状态
|
||||
setState(danmu, state) {
|
||||
setState(danmu: DanmuItem, state: DanmuState) {
|
||||
setItemState(this, danmu, state)
|
||||
}
|
||||
|
||||
// 重置弹幕到wait状态,回收弹幕DOM节点
|
||||
makeWait(danmu) {
|
||||
makeWait(danmu: DanmuItem) {
|
||||
this.renderer.makeWait(danmu)
|
||||
}
|
||||
|
||||
@@ -454,7 +479,7 @@ export default class Danmuku {
|
||||
|
||||
show() {
|
||||
this.isHide = false
|
||||
this.$danmuku.style.opacity = 1
|
||||
this.$danmuku.style.opacity = 1 as unknown as string
|
||||
this.option.visible = true
|
||||
this.art.emit('artplayerPluginDanmuku:show')
|
||||
return this
|
||||
@@ -463,7 +488,7 @@ export default class Danmuku {
|
||||
hide() {
|
||||
this.isHide = true
|
||||
this.scheduler.invalidate()
|
||||
this.$danmuku.style.opacity = 0
|
||||
this.$danmuku.style.opacity = 0 as unknown as string
|
||||
this.option.visible = false
|
||||
this.art.emit('artplayerPluginDanmuku:hide')
|
||||
this.scheduler.schedule()
|
||||
+31
-12
@@ -1,19 +1,36 @@
|
||||
import type { DanmuItem, HeatmapOptions, HeatmapPoint } from './types'
|
||||
import { MAX_HEATMAP_POINTS, sampleHeatmap } from './heatmap-sampling'
|
||||
|
||||
function map(value, inMin, inMax, outMin, outMax) {
|
||||
function map(value: number, inMin: number, inMax: number, outMin: number, outMax: number): number {
|
||||
return ((value - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin
|
||||
}
|
||||
|
||||
function line(pointA, pointB) {
|
||||
function line(pointA: HeatmapPoint, pointB: HeatmapPoint) {
|
||||
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 = [] }) {
|
||||
interface GeometryInput {
|
||||
width: number
|
||||
height: number
|
||||
duration: number
|
||||
queue: readonly Pick<DanmuItem, 'time'>[]
|
||||
option: boolean | HeatmapOptions
|
||||
points?: HeatmapPoint[]
|
||||
}
|
||||
|
||||
export interface HeatmapGeometry {
|
||||
path: string
|
||||
opacity: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export function heatmapGeometry({ width, height, duration, queue, option, points: input = [] }: GeometryInput): HeatmapGeometry | null {
|
||||
if (![width, height, duration].every(value => Number.isFinite(value) && value > 0))
|
||||
return null
|
||||
const defaults = {
|
||||
const defaults: Required<HeatmapOptions> = {
|
||||
xMin: 0,
|
||||
xMax: width,
|
||||
yMin: 0,
|
||||
@@ -25,9 +42,9 @@ export function heatmapGeometry({ width, height, duration, queue, option, points
|
||||
smoothing: 0.2,
|
||||
flattening: 0.2,
|
||||
}
|
||||
const configured = Object.assign({}, typeof option === 'object' ? option : {})
|
||||
const configured: HeatmapOptions = Object.assign({}, typeof option === 'object' ? option : {})
|
||||
const options = Object.assign({}, defaults, configured)
|
||||
for (const key of Object.keys(defaults)) {
|
||||
for (const key of Object.keys(defaults) as (keyof HeatmapOptions)[]) {
|
||||
if (!Number.isFinite(options[key]))
|
||||
options[key] = defaults[key]
|
||||
}
|
||||
@@ -39,7 +56,8 @@ export function heatmapGeometry({ width, height, duration, queue, option, points
|
||||
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]
|
||||
// The nonempty check above guarantees both endpoints used below.
|
||||
const lastPoint = points[points.length - 1]!
|
||||
if (lastPoint[0] !== width) {
|
||||
if (points.length === MAX_HEATMAP_POINTS)
|
||||
return null
|
||||
@@ -65,8 +83,8 @@ export function heatmapGeometry({ width, height, duration, queue, option, points
|
||||
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 boundY = (value: number) => fitted ? Math.min(height, Math.max(height * 0.75, value)) : value
|
||||
const controlPoint = (current: HeatmapPoint, previous: HeatmapPoint | undefined, next: HeatmapPoint | undefined, reverse?: boolean): HeatmapPoint => {
|
||||
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)
|
||||
@@ -74,13 +92,14 @@ export function heatmapGeometry({ width, height, duration, queue, option, points
|
||||
// A cubic stays inside the convex hull of its endpoints and controls.
|
||||
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))])
|
||||
const positions = points.map<HeatmapPoint>(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)
|
||||
return `M ${all[all.length - 1]![0]},${height} L ${point[0]},${height} L ${point[0]},${point[1]}`
|
||||
// Every noninitial point has its immediately preceding control point.
|
||||
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(' ')
|
||||
+7
-4
@@ -1,22 +1,25 @@
|
||||
import type { DanmuItem, HeatmapPoint } from './types'
|
||||
|
||||
export const MAX_HEATMAP_POINTS = 0xFFFFFFFF
|
||||
|
||||
function upperBound(values, value) {
|
||||
function upperBound(values: readonly number[], value: number): number {
|
||||
let start = 0
|
||||
let end = values.length
|
||||
while (start < end) {
|
||||
const middle = Math.floor((start + end) / 2)
|
||||
if (values[middle] <= value)
|
||||
// The binary-search interval guarantees middle is an existing index.
|
||||
if (values[middle]! <= value)
|
||||
start = middle + 1
|
||||
else end = middle
|
||||
}
|
||||
return start
|
||||
}
|
||||
|
||||
export function sampleHeatmap(queue, width, duration, sampling) {
|
||||
export function sampleHeatmap(queue: readonly Pick<DanmuItem, 'time'>[], width: number, duration: number, sampling: number): HeatmapPoint[] {
|
||||
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 points: HeatmapPoint[] = []
|
||||
const gap = duration / width
|
||||
if (!Number.isFinite(gap))
|
||||
return []
|
||||
+38
-21
@@ -1,32 +1,47 @@
|
||||
import type { DanmuItem, DanmukuArt, HeatmapOptions, HeatmapPoint } from './types'
|
||||
import { heatmapGeometry } from './heatmap-geometry'
|
||||
|
||||
let nextGradient = 0
|
||||
|
||||
export default function heatmap(art, danmuku, option) {
|
||||
type HeatmapArt = Pick<DanmukuArt, 'isDestroy' | 'constructor' | 'on' | 'off' | 'controls' | 'option' | 'duration' | 'played'>
|
||||
interface HeatmapEvents {
|
||||
'destroy': []
|
||||
'video:timeupdate': []
|
||||
'setBar': [type: string, percentage: number]
|
||||
'ready': []
|
||||
'resize': []
|
||||
'artplayerPluginDanmuku:loaded': []
|
||||
'artplayerPluginDanmuku:points': [points: HeatmapPoint[]]
|
||||
}
|
||||
type HeatmapCallback = (...args: unknown[]) => unknown
|
||||
|
||||
export default function heatmap(art: HeatmapArt, danmuku: { queue: readonly Pick<DanmuItem, 'time'>[] }, option: boolean | HeatmapOptions): (() => void) | undefined {
|
||||
if (art.isDestroy)
|
||||
return
|
||||
const { query } = art.constructor.utils
|
||||
let gradient
|
||||
const subscriptions = []
|
||||
let element
|
||||
let start
|
||||
let stop
|
||||
let gradient: string | undefined
|
||||
const subscriptions: [keyof HeatmapEvents, HeatmapCallback][] = []
|
||||
let element: HTMLElement | undefined
|
||||
let start: Element | null | undefined
|
||||
let stop: Element | null | undefined
|
||||
let closed = false
|
||||
|
||||
const active = () => !closed && !art.isDestroy
|
||||
function listen(name, callback) {
|
||||
subscriptions.push([name, callback])
|
||||
art.on(name, callback)
|
||||
function listen<Name extends keyof HeatmapEvents>(name: Name, callback: (...args: HeatmapEvents[Name]) => unknown) {
|
||||
// Core's string-event overload erases payloads; this local map checks each subscription.
|
||||
const listener = callback as HeatmapCallback
|
||||
subscriptions.push([name, listener])
|
||||
art.on(name, listener)
|
||||
}
|
||||
function dispose(removeControl) {
|
||||
function dispose(removeControl: boolean) {
|
||||
if (closed)
|
||||
return
|
||||
closed = true
|
||||
start = null
|
||||
stop = null
|
||||
let failure
|
||||
let failure: unknown
|
||||
let failed = false
|
||||
const attempt = (callback) => {
|
||||
const attempt = (callback: () => unknown) => {
|
||||
try {
|
||||
callback()
|
||||
}
|
||||
@@ -44,29 +59,31 @@ export default function heatmap(art, danmuku, option) {
|
||||
if (failed)
|
||||
throw failure
|
||||
}
|
||||
function progress(value) {
|
||||
function progress(value: number) {
|
||||
if (active() && start && stop) {
|
||||
start.setAttribute('offset', `${value * 100}%`)
|
||||
stop.setAttribute('offset', `${value * 100}%`)
|
||||
}
|
||||
}
|
||||
function draw(points = []) {
|
||||
function draw(points: HeatmapPoint[] = []) {
|
||||
if (!active())
|
||||
return
|
||||
start = null
|
||||
stop = null
|
||||
element.innerHTML = ''
|
||||
// Drawing listeners are registered only after mounted assigns element; dispose closes first.
|
||||
const target = element!
|
||||
target.innerHTML = ''
|
||||
if (art.option.isLive)
|
||||
return
|
||||
const shape = heatmapGeometry({ width: element.offsetWidth, height: element.offsetHeight, duration: art.duration, queue: danmuku.queue, option, points })
|
||||
const shape = heatmapGeometry({ width: target.offsetWidth, height: target.offsetHeight, duration: art.duration, queue: danmuku.queue, option, points })
|
||||
if (!shape || !active())
|
||||
return
|
||||
const document = element.ownerDocument
|
||||
const document = target.ownerDocument
|
||||
if (!gradient)
|
||||
gradient = document ? 'heatmap-solids' : `heatmap-solids-${++nextGradient}`
|
||||
while (document?.getElementById(gradient))
|
||||
gradient = `heatmap-solids-${++nextGradient}`
|
||||
element.innerHTML = `
|
||||
target.innerHTML = `
|
||||
<svg viewBox="0 0 ${shape.width} ${shape.height}">
|
||||
<defs>
|
||||
<linearGradient id="${gradient}" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
@@ -79,11 +96,11 @@ export default function heatmap(art, danmuku, option) {
|
||||
<path fill="url(#${gradient})" d="${shape.path}"></path>
|
||||
</svg>
|
||||
`
|
||||
start = query('#heatmap-start', element)
|
||||
stop = query('#heatmap-stop', element)
|
||||
start = query('#heatmap-start', target)
|
||||
stop = query('#heatmap-stop', target)
|
||||
progress(art.played)
|
||||
}
|
||||
function update(points) {
|
||||
function update(points?: HeatmapPoint[]) {
|
||||
try {
|
||||
draw(points)
|
||||
}
|
||||
+11
-7
@@ -1,15 +1,19 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
import type { DanmukuArt, DanmukuOption } from './types'
|
||||
|
||||
import Danmuku from './danmuku'
|
||||
import heatmap from './heatmap'
|
||||
import Setting from './setting'
|
||||
|
||||
export default function artplayerPluginDanmuku(option) {
|
||||
return (art) => {
|
||||
const danmuku = new Danmuku(art, option)
|
||||
let setting
|
||||
export default function artplayerPluginDanmuku(option: DanmukuOption) {
|
||||
return (art: Artplayer) => {
|
||||
// Old declarations type instance.constructor as Function; both supported cores expose these statics.
|
||||
const danmuku = new Danmuku(art as DanmukuArt, option)
|
||||
let setting: Setting | undefined
|
||||
try {
|
||||
setting = art.isDestroy ? undefined : new Setting(art, danmuku)
|
||||
setting = art.isDestroy ? undefined : new Setting(art as DanmukuArt, danmuku)
|
||||
if (!art.isDestroy && danmuku.option.heatmap) {
|
||||
heatmap(art, danmuku, danmuku.option.heatmap)
|
||||
heatmap(art as DanmukuArt, danmuku, danmuku.option.heatmap)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
@@ -25,7 +29,7 @@ export default function artplayerPluginDanmuku(option) {
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'artplayerPluginDanmuku',
|
||||
name: 'artplayerPluginDanmuku' as const,
|
||||
emit: danmuku.emit.bind(danmuku),
|
||||
load: danmuku.load.bind(danmuku),
|
||||
config: danmuku.config.bind(danmuku),
|
||||
+40
-15
@@ -1,22 +1,45 @@
|
||||
import type { DanmuInput, DanmukuInput } from './types'
|
||||
import { bilibiliDanmuParseFromUrl } from './bilibili'
|
||||
import { isPromiseInput } from './config'
|
||||
|
||||
const inputs = new WeakMap()
|
||||
const cancelled = Symbol('cancelled danmuku input')
|
||||
|
||||
function stateFor(owner) {
|
||||
if (!inputs.has(owner))
|
||||
inputs.set(owner, { closed: false, tasks: new Set(), emitting: [] })
|
||||
return inputs.get(owner)
|
||||
interface InputOwner {
|
||||
art: { isDestroy: boolean }
|
||||
emit: (danmu: DanmuInput) => unknown
|
||||
}
|
||||
|
||||
export function inputActive(owner) {
|
||||
export interface InputTask {
|
||||
replace: boolean
|
||||
signal: AbortSignal | undefined
|
||||
active: () => boolean
|
||||
onCancel: (callback: () => void) => () => void
|
||||
cancel: () => void
|
||||
wait: <T>(value: T) => Promise<Awaited<T> | typeof cancelled>
|
||||
emit: (danmu: DanmuInput) => unknown
|
||||
finish: () => void
|
||||
}
|
||||
|
||||
interface InputState {
|
||||
closed: boolean
|
||||
tasks: Set<InputTask>
|
||||
emitting: InputTask[]
|
||||
}
|
||||
|
||||
const inputs = new WeakMap<InputOwner, InputState>()
|
||||
export const cancelled = Symbol('cancelled danmuku input')
|
||||
|
||||
function stateFor(owner: InputOwner): InputState {
|
||||
if (!inputs.has(owner))
|
||||
inputs.set(owner, { closed: false, tasks: new Set(), emitting: [] })
|
||||
return inputs.get(owner)!
|
||||
}
|
||||
|
||||
export function inputActive(owner: InputOwner) {
|
||||
const state = stateFor(owner)
|
||||
const task = state.emitting[state.emitting.length - 1]
|
||||
return !state.closed && !owner.art.isDestroy && (!task || task.active())
|
||||
}
|
||||
|
||||
export function beginInput(owner, replace) {
|
||||
export function beginInput(owner: InputOwner, replace: boolean): InputTask {
|
||||
const state = stateFor(owner)
|
||||
if (replace) {
|
||||
for (const task of [...state.tasks]) {
|
||||
@@ -25,11 +48,11 @@ export function beginInput(owner, replace) {
|
||||
}
|
||||
}
|
||||
let stopped = state.closed || owner.art.isDestroy
|
||||
let resolveCancel
|
||||
const cancellation = new Promise(resolve => resolveCancel = resolve)
|
||||
const handlers = new Set()
|
||||
let resolveCancel!: (value: typeof cancelled) => void
|
||||
const cancellation = new Promise<typeof cancelled>(resolve => resolveCancel = resolve)
|
||||
const handlers = new Set<() => void>()
|
||||
const controller = typeof AbortController === 'function' ? new AbortController() : undefined
|
||||
const task = {
|
||||
const task: InputTask = {
|
||||
replace,
|
||||
signal: controller?.signal,
|
||||
active: () => !stopped && !state.closed && !owner.art.isDestroy,
|
||||
@@ -71,13 +94,15 @@ export function beginInput(owner, replace) {
|
||||
return task
|
||||
}
|
||||
|
||||
export function cancelInputs(owner) {
|
||||
export function cancelInputs(owner: InputOwner) {
|
||||
const state = stateFor(owner)
|
||||
state.closed = true
|
||||
for (const task of [...state.tasks]) task.cancel()
|
||||
}
|
||||
|
||||
export function readInput(target, task) {
|
||||
export function readInput(target: DanmukuInput, task: InputTask):
|
||||
| { asynchronous: true, value: DanmuInput[] | PromiseLike<DanmuInput[]> }
|
||||
| { asynchronous: false, value: DanmuInput[] } {
|
||||
if (typeof target === 'function')
|
||||
return { asynchronous: true, value: target() }
|
||||
if (isPromiseInput(target))
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { DanmuInput } from './types'
|
||||
|
||||
export interface ParsedDanmu extends DanmuInput {
|
||||
time: number
|
||||
mode: number
|
||||
fontSize: number
|
||||
color: string
|
||||
timestamp: number
|
||||
pool: number
|
||||
userID: string
|
||||
rowID: number
|
||||
}
|
||||
|
||||
export interface ParserRequest { xml: unknown, id: number }
|
||||
export interface ParserReply { danmus: ParsedDanmu[], id: number }
|
||||
|
||||
export interface ParserCancellation {
|
||||
signal?: AbortSignal
|
||||
onCancel?: (callback: () => void) => () => void
|
||||
}
|
||||
+9
-5
@@ -1,12 +1,16 @@
|
||||
export function filterState(owner, state, callback) {
|
||||
import type { DanmuItem, DanmuState } from './types'
|
||||
|
||||
interface QueueOwner { states: Record<DanmuState, DanmuItem[]>, art: { currentTime: number } }
|
||||
|
||||
export function filterState(owner: QueueOwner, state: DanmuState, callback: (danmu: DanmuItem) => void) {
|
||||
const danmus = owner.states[state] || []
|
||||
for (let index = 0; index < danmus.length; index++) callback(danmus[index])
|
||||
for (let index = 0; index < danmus.length; index++) callback(danmus[index]!)
|
||||
return danmus
|
||||
}
|
||||
|
||||
export function readyItems(owner) {
|
||||
export function readyItems(owner: QueueOwner) {
|
||||
const { currentTime } = owner.art
|
||||
const result = []
|
||||
const result: DanmuItem[] = []
|
||||
filterState(owner, 'ready', danmu => result.push(danmu))
|
||||
filterState(owner, 'wait', (danmu) => {
|
||||
if (currentTime + 0.1 >= danmu.time && danmu.time >= currentTime - 0.1)
|
||||
@@ -15,7 +19,7 @@ export function readyItems(owner) {
|
||||
return result
|
||||
}
|
||||
|
||||
export function setItemState(owner, danmu, state) {
|
||||
export function setItemState(owner: QueueOwner, danmu: DanmuItem, state: DanmuState) {
|
||||
owner.states[danmu.$state] = owner.states[danmu.$state].filter(item => item !== danmu)
|
||||
danmu.$state = state
|
||||
if (danmu.$ref)
|
||||
+43
-36
@@ -1,5 +1,12 @@
|
||||
import type Danmuku from './danmuku'
|
||||
import type { FrameOperation } from './scheduler'
|
||||
import type { DanmuItem } from './types'
|
||||
|
||||
export default class Renderer {
|
||||
constructor(owner) {
|
||||
declare owner: Danmuku
|
||||
declare nodes: Set<HTMLDivElement>
|
||||
|
||||
constructor(owner: Danmuku) {
|
||||
this.owner = owner
|
||||
this.nodes = new Set()
|
||||
}
|
||||
@@ -14,21 +21,21 @@ export default class Renderer {
|
||||
return $ref
|
||||
}
|
||||
|
||||
get visibles() {
|
||||
const result = []
|
||||
get visibles(): { top: number, left: number, height: number, width: number, right: number, speed: number, distance: number, time: number, mode: number }[] {
|
||||
const result: Renderer['visibles'] = []
|
||||
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 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 = {}
|
||||
const emit = {} as Renderer['visibles'][number]
|
||||
emit.top = top
|
||||
emit.left = left
|
||||
emit.height = height
|
||||
@@ -45,21 +52,21 @@ export default class Renderer {
|
||||
return result
|
||||
}
|
||||
|
||||
left($ref) {
|
||||
left($ref: HTMLElement): number {
|
||||
const rect = $ref.getBoundingClientRect()
|
||||
return this.owner.isRotate ? rect.top : rect.left
|
||||
}
|
||||
|
||||
makeWait(danmu) {
|
||||
makeWait(danmu: DanmuItem) {
|
||||
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'
|
||||
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
|
||||
danmu!.$ref = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +77,7 @@ export default class Renderer {
|
||||
switch (danmu.mode) {
|
||||
// 滚动的弹幕
|
||||
case 0:
|
||||
danmu.$ref.style.left = `${clientWidth}px`
|
||||
danmu.$ref!.style.left = `${clientWidth}px`
|
||||
break
|
||||
default:
|
||||
break
|
||||
@@ -82,10 +89,10 @@ export default class Renderer {
|
||||
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`
|
||||
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:
|
||||
@@ -102,9 +109,9 @@ export default class Renderer {
|
||||
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`
|
||||
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:
|
||||
@@ -122,9 +129,9 @@ export default class Renderer {
|
||||
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'
|
||||
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:
|
||||
@@ -135,7 +142,7 @@ export default class Renderer {
|
||||
return this
|
||||
}
|
||||
|
||||
prepare(danmu, operation) {
|
||||
prepare(danmu: DanmuItem, operation: FrameOperation) {
|
||||
const owner = this.owner
|
||||
const ref = owner.$ref
|
||||
operation.danmu = danmu
|
||||
@@ -143,19 +150,19 @@ export default class Renderer {
|
||||
danmu.$ref = ref
|
||||
ref.textContent = danmu.text
|
||||
owner.$danmuku.appendChild(ref)
|
||||
ref.style.opacity = owner.option.opacity
|
||||
ref.style.opacity = owner.option.opacity as unknown as string
|
||||
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
|
||||
ref.style.border = danmu.border ? `1px solid ${danmu.color}` : null as unknown as string
|
||||
ref.style.backgroundColor = danmu.border ? 'rgb(0 0 0 / 50%)' : null as unknown as string
|
||||
owner.utils.setStyles(ref, danmu.style)
|
||||
return ref
|
||||
}
|
||||
|
||||
place(danmu, ref, top, distance, clientWidth) {
|
||||
place(danmu: DanmuItem, ref: HTMLDivElement, top: number, distance: number, clientWidth: number) {
|
||||
ref.style.top = `${top}px`
|
||||
ref.style.visibility = 'visible'
|
||||
ref.dataset.mode = danmu.mode
|
||||
ref.dataset.mode = danmu.mode as unknown as string
|
||||
ref.dataset.id = danmu.id || ''
|
||||
switch (danmu.mode) {
|
||||
case 0:
|
||||
@@ -174,13 +181,13 @@ export default class Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
release(operation) {
|
||||
release(operation: FrameOperation) {
|
||||
const { danmu, ref } = operation
|
||||
if (ref && danmu.$ref === ref) {
|
||||
if (ref && danmu!.$ref === ref) {
|
||||
ref.style.cssText = this.owner.constructor.cssText
|
||||
ref.style.visibility = 'hidden'
|
||||
this.owner.$refs.push(ref)
|
||||
danmu.$ref = null
|
||||
danmu!.$ref = null
|
||||
}
|
||||
operation.ref = null
|
||||
}
|
||||
+34
-11
@@ -1,7 +1,28 @@
|
||||
import type Danmuku from './danmuku'
|
||||
import type { DanmuItem } from './types'
|
||||
|
||||
const cancelled = Symbol('cancelled danmuku frame')
|
||||
|
||||
export interface FrameOperation {
|
||||
generation: number
|
||||
cancel: () => void
|
||||
wait: <T>(value: T | PromiseLike<T>) => Promise<T | typeof cancelled>
|
||||
danmu?: DanmuItem
|
||||
ref: HTMLDivElement | null
|
||||
}
|
||||
|
||||
export default class Scheduler {
|
||||
constructor(owner) {
|
||||
declare owner: Danmuku
|
||||
declare generation: number
|
||||
declare starts: number
|
||||
declare frame: number | null
|
||||
declare operation: FrameOperation | null
|
||||
declare closed: boolean
|
||||
declare running: boolean
|
||||
declare fault: boolean
|
||||
declare failedItems: Set<DanmuItem>
|
||||
|
||||
constructor(owner: Danmuku) {
|
||||
this.owner = owner
|
||||
this.generation = 0
|
||||
this.starts = 0
|
||||
@@ -13,13 +34,13 @@ export default class Scheduler {
|
||||
this.failedItems = new Set()
|
||||
}
|
||||
|
||||
active(operation) {
|
||||
active(operation: FrameOperation) {
|
||||
const owner = this.owner
|
||||
return !this.closed && !this.fault && !owner.art.isDestroy && !owner.isStop
|
||||
&& !owner.isHide && operation.generation === this.generation
|
||||
}
|
||||
|
||||
release(operation) {
|
||||
release(operation: FrameOperation) {
|
||||
this.owner.renderer.release(operation)
|
||||
}
|
||||
|
||||
@@ -39,7 +60,7 @@ export default class Scheduler {
|
||||
this.failedItems.clear()
|
||||
}
|
||||
|
||||
report(error) {
|
||||
report(error: unknown) {
|
||||
try {
|
||||
this.owner.art.emit('artplayerPluginDanmuku:error', error)
|
||||
}
|
||||
@@ -48,7 +69,7 @@ export default class Scheduler {
|
||||
}
|
||||
}
|
||||
|
||||
fail(error) {
|
||||
fail(error: unknown) {
|
||||
if (this.closed || this.fault)
|
||||
return
|
||||
this.fault = true
|
||||
@@ -81,9 +102,9 @@ export default class Scheduler {
|
||||
owner.timer = null
|
||||
if (this.closed || this.fault || owner.isStop || owner.art.isDestroy)
|
||||
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 }
|
||||
let cancel!: () => void
|
||||
const cancellation = new Promise<typeof cancelled>(resolve => cancel = () => resolve(cancelled))
|
||||
const operation: FrameOperation = { generation: this.generation, cancel, wait: value => Promise.race([value, cancellation]), ref: null }
|
||||
this.operation = operation
|
||||
return this.run(operation).catch((error) => {
|
||||
if (this.active(operation))
|
||||
@@ -99,7 +120,7 @@ export default class Scheduler {
|
||||
owner.timer = this.frame
|
||||
}
|
||||
|
||||
async run(operation) {
|
||||
async run(operation: FrameOperation) {
|
||||
const owner = this.owner
|
||||
if (!owner.art.playing || !this.active(operation))
|
||||
return
|
||||
@@ -139,16 +160,18 @@ export default class Scheduler {
|
||||
danmu.$lastStartTime = Date.now()
|
||||
danmu.$restTime = owner.speed
|
||||
const distance = clientWidth + ref.clientWidth
|
||||
const { result: top } = await operation.wait(owner.postMessage({
|
||||
const reply = await operation.wait(owner.postMessage({
|
||||
type: 'getDanmuTop',
|
||||
target: { mode: danmu.mode, height: ref.clientHeight, speed: distance / danmu.$restTime },
|
||||
visibles: owner.visibles,
|
||||
antiOverlap: owner.option.antiOverlap,
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
marginBottom: owner.marginBottom,
|
||||
// Valid margins are numeric. Preserve the old raw fallback for unsupported strings.
|
||||
marginBottom: owner.marginBottom as number,
|
||||
marginTop: owner.marginTop,
|
||||
}))
|
||||
const top = typeof reply === 'symbol' ? undefined : reply.result
|
||||
if (!this.active(operation) || danmu.$ref !== ref)
|
||||
return
|
||||
if (top !== undefined) {
|
||||
+49
-15
@@ -1,7 +1,35 @@
|
||||
const properties = new WeakMap()
|
||||
import type { DanmukuArt } from './types'
|
||||
|
||||
type Disposer = () => unknown
|
||||
type PropertyValue = string | number | boolean | undefined
|
||||
interface PropertyEntry {
|
||||
original: PropertyValue
|
||||
present: boolean
|
||||
priority: string | undefined
|
||||
owners: Map<SettingLifecycle, PropertyValue>
|
||||
apply: (value: PropertyValue) => unknown
|
||||
last?: PropertyValue
|
||||
lastPriority?: string
|
||||
}
|
||||
interface SettingEvents {
|
||||
'resize': []
|
||||
'fullscreen': [state: boolean]
|
||||
'fullscreenWeb': [state: boolean]
|
||||
'artplayerPluginDanmuku:show': []
|
||||
'artplayerPluginDanmuku:hide': []
|
||||
'document:pointermove': [event: PointerEvent]
|
||||
'document:pointerup': [event: PointerEvent]
|
||||
}
|
||||
const properties = new WeakMap<object, Map<string, PropertyEntry>>()
|
||||
|
||||
export default class SettingLifecycle {
|
||||
constructor(art) {
|
||||
declare art: DanmukuArt
|
||||
declare closed: boolean
|
||||
declare disposers: Set<Disposer>
|
||||
declare cancelled: Promise<void>
|
||||
declare cancel: () => void
|
||||
|
||||
constructor(art: DanmukuArt) {
|
||||
this.art = art
|
||||
this.closed = false
|
||||
this.disposers = new Set()
|
||||
@@ -12,13 +40,15 @@ export default class SettingLifecycle {
|
||||
return !this.closed && !this.art.isDestroy
|
||||
}
|
||||
|
||||
own(dispose) {
|
||||
own(dispose: Disposer) {
|
||||
if (this.closed)
|
||||
dispose()
|
||||
else this.disposers.add(dispose)
|
||||
}
|
||||
|
||||
write(target, key, value, apply = value => target[key] = value) {
|
||||
write(host: DOMStringMap | CSSStyleDeclaration, key: string, value: PropertyValue, apply: (value: PropertyValue) => unknown = value => (host as unknown as Record<string, PropertyValue>)[key] = value) {
|
||||
// This adapter retains native dataset/style assignment and its WebIDL coercion.
|
||||
const target = host as unknown as Record<string, PropertyValue> & Partial<Pick<CSSStyleDeclaration, 'getPropertyPriority' | 'setProperty'>>
|
||||
if (!this.active)
|
||||
return
|
||||
const priority = () => typeof target.getPropertyPriority === 'function' ? target.getPropertyPriority(key) : undefined
|
||||
@@ -52,7 +82,7 @@ export default class SettingLifecycle {
|
||||
entry.lastPriority = priority()
|
||||
}
|
||||
else if (entry.priority !== undefined) {
|
||||
target.setProperty(key, entry.original, entry.priority)
|
||||
target.setProperty!(key, entry.original as string, entry.priority)
|
||||
}
|
||||
else if (entry.present) {
|
||||
entry.apply(entry.original)
|
||||
@@ -75,13 +105,15 @@ export default class SettingLifecycle {
|
||||
entry.lastPriority = priority()
|
||||
}
|
||||
|
||||
on(name, callback) {
|
||||
on<Name extends keyof SettingEvents>(name: Name, callback: (this: unknown, ...args: SettingEvents[Name]) => unknown) {
|
||||
if (!this.active)
|
||||
return
|
||||
// Keep the emitter/native callback receiver while consulting this owner.
|
||||
// eslint-disable-next-line ts/no-this-alias
|
||||
const lifecycle = this
|
||||
function listener(...args) {
|
||||
function listener(this: unknown, ...args: unknown[]) {
|
||||
if (lifecycle.active)
|
||||
return callback.apply(this, args)
|
||||
return callback.apply(this, args as SettingEvents[Name])
|
||||
}
|
||||
const dispose = () => this.art.off(name, listener)
|
||||
this.own(dispose)
|
||||
@@ -96,18 +128,20 @@ export default class SettingLifecycle {
|
||||
}
|
||||
}
|
||||
|
||||
proxy(target, name, callback) {
|
||||
proxy<Name extends keyof HTMLElementEventMap>(target: HTMLElement, name: Name, callback: (this: unknown, event: HTMLElementEventMap[Name]) => unknown) {
|
||||
if (!this.active)
|
||||
return
|
||||
// Keep the emitter/native callback receiver while consulting this owner.
|
||||
// eslint-disable-next-line ts/no-this-alias
|
||||
const lifecycle = this
|
||||
function listener(...args) {
|
||||
function listener(this: unknown, ...args: unknown[]) {
|
||||
if (lifecycle.active)
|
||||
return callback.apply(this, args)
|
||||
return callback.apply(this, args as [HTMLElementEventMap[Name]])
|
||||
}
|
||||
let remove
|
||||
let remove: (() => void) | undefined
|
||||
const dispose = () => {
|
||||
if (typeof remove === 'function') {
|
||||
const registry = this.art.events
|
||||
const registry = this.art.events as DanmukuArt['events'] & { destroyEvents?: Set<() => void> }
|
||||
if (typeof registry?.remove === 'function' && registry.destroyEvents?.has(remove))
|
||||
registry.remove(remove)
|
||||
else remove()
|
||||
@@ -128,11 +162,11 @@ export default class SettingLifecycle {
|
||||
}
|
||||
}
|
||||
|
||||
wait(value) {
|
||||
wait<Value>(value: Value | PromiseLike<Value>) {
|
||||
return Promise.race([value, this.cancelled])
|
||||
}
|
||||
|
||||
release(dispose) {
|
||||
release(dispose: Disposer) {
|
||||
try {
|
||||
dispose()
|
||||
}
|
||||
+12
-9
@@ -1,17 +1,20 @@
|
||||
export async function emitSetting(setting) {
|
||||
import type Setting from './setting'
|
||||
import type { DanmuInput } from './types'
|
||||
|
||||
export async function emitSetting(setting: 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 = {
|
||||
const danmu: DanmuInput = {
|
||||
text,
|
||||
mode: setting.option.mode,
|
||||
color: setting.option.color,
|
||||
time: setting.art.currentTime,
|
||||
}
|
||||
const report = (error) => {
|
||||
const report = (error: unknown) => {
|
||||
if (lifecycle.active)
|
||||
console.error('Error emitting danmuku:', error)
|
||||
}
|
||||
@@ -39,15 +42,15 @@ export async function emitSetting(setting) {
|
||||
}
|
||||
}
|
||||
|
||||
export function lockSetting(setting) {
|
||||
export function lockSetting(setting: Setting) {
|
||||
if (!setting.lifecycle.active)
|
||||
return
|
||||
const { addClass } = setting.utils
|
||||
const { $send } = setting.template
|
||||
clearTimeout(setting.timer)
|
||||
clearTimeout(setting.timer as number | undefined)
|
||||
setting.isLock = true
|
||||
let time = setting.option.lockTime
|
||||
$send.textContent = time
|
||||
$send.textContent = time as unknown as string
|
||||
addClass($send, 'apd-lock')
|
||||
const loop = () => {
|
||||
setting.timer = setTimeout(() => {
|
||||
@@ -58,7 +61,7 @@ export function lockSetting(setting) {
|
||||
}
|
||||
else {
|
||||
time -= 1
|
||||
$send.textContent = time
|
||||
$send.textContent = time as unknown as string
|
||||
loop()
|
||||
}
|
||||
}, 1000)
|
||||
@@ -66,8 +69,8 @@ export function lockSetting(setting) {
|
||||
loop()
|
||||
}
|
||||
|
||||
export function unlockSetting(setting) {
|
||||
clearTimeout(setting.timer)
|
||||
export function unlockSetting(setting: Setting) {
|
||||
clearTimeout(setting.timer as number | undefined)
|
||||
setting.timer = null
|
||||
setting.isLock = false
|
||||
if (!setting.lifecycle.active)
|
||||
+9
-6
@@ -1,10 +1,13 @@
|
||||
export default function createSlider({ min, max, container, findIndex, onChange, steps = [] }) {
|
||||
import type Setting from './setting'
|
||||
import type { SliderConfig } from './setting-types'
|
||||
|
||||
export default function createSlider(this: Setting, { min, max, container, findIndex, onChange, steps = [] }: SliderConfig) {
|
||||
const { lifecycle } = this
|
||||
if (!lifecycle.active)
|
||||
return { reset() {} }
|
||||
const { query, clamp, setStyle } = this.utils
|
||||
|
||||
setStyle(container, 'touch-action', 'none')
|
||||
setStyle(container, 'touch-action' as keyof CSSStyleDeclaration, 'none')
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="apd-slider-line">
|
||||
@@ -19,14 +22,14 @@ export default function createSlider({ min, max, container, findIndex, onChange,
|
||||
</div>
|
||||
`
|
||||
|
||||
const $dot = query('.apd-slider-dot', container)
|
||||
const $progress = query('.apd-slider-progress', container)
|
||||
const $dot = query<HTMLDivElement>('.apd-slider-dot', container)!
|
||||
const $progress = query<HTMLDivElement>('.apd-slider-progress', container)!
|
||||
|
||||
let isDroging = false
|
||||
|
||||
lifecycle.own(() => isDroging = false)
|
||||
|
||||
function reset(index) {
|
||||
function reset(index?: number) {
|
||||
if (!lifecycle.active)
|
||||
return
|
||||
if (index === undefined)
|
||||
@@ -41,7 +44,7 @@ export default function createSlider({ min, max, container, findIndex, onChange,
|
||||
onChange(index)
|
||||
}
|
||||
|
||||
function updateLeft(event) {
|
||||
function updateLeft(this: Setting, event: MouseEvent) {
|
||||
const { top, height, left, width } = container.getBoundingClientRect()
|
||||
if (this.art.isRotate) {
|
||||
const value = clamp(event.clientY - top, 0, height)
|
||||
+8
-7
@@ -3,26 +3,27 @@ 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]
|
||||
const styleDocument = document as Document & { [key: symbol]: HTMLElement | undefined }
|
||||
let $style = document.getElementById(id) || styleDocument[pending]
|
||||
if (!$style) {
|
||||
$style = document.createElement('style')
|
||||
$style.id = id
|
||||
if (document.readyState === 'loading') {
|
||||
document[pending] = $style
|
||||
styleDocument[pending] = $style
|
||||
const ready = () => {
|
||||
document.removeEventListener('DOMContentLoaded', ready)
|
||||
if (document[pending] === $style)
|
||||
delete document[pending]
|
||||
if (styleDocument[pending] === $style)
|
||||
delete styleDocument[pending]
|
||||
const existing = document.getElementById(id)
|
||||
if (existing)
|
||||
existing.textContent = $style.textContent
|
||||
existing.textContent = $style!.textContent
|
||||
else
|
||||
(document.head || document.documentElement).appendChild($style)
|
||||
(document.head || document.documentElement).appendChild($style!)
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', ready, { once: true })
|
||||
}
|
||||
else {
|
||||
(document.head || document.documentElement).appendChild($style)
|
||||
(document.head || document.documentElement).appendChild($style!)
|
||||
}
|
||||
}
|
||||
$style.textContent = style
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
import type Setting from './setting'
|
||||
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'
|
||||
@@ -28,7 +29,7 @@ export function settingIcons() {
|
||||
}
|
||||
}
|
||||
|
||||
export function settingTemplate() {
|
||||
export function settingTemplate(this: Setting) {
|
||||
const { option } = this
|
||||
return `
|
||||
<div class="apd-toggle">
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { SliderStep } from './types'
|
||||
|
||||
// createTemplate/createSliders fill these slots before registering active UI callbacks.
|
||||
// The initial null values and absent antiOverlap/syncVideo keys are kept for compatibility.
|
||||
export interface SettingTemplate {
|
||||
$controlsCenter: HTMLDivElement
|
||||
$mount: HTMLElement
|
||||
$danmuku: HTMLDivElement
|
||||
$toggle: HTMLDivElement
|
||||
$config: HTMLDivElement
|
||||
$configPanel: HTMLDivElement
|
||||
$configModes: HTMLDivElement
|
||||
$style: HTMLDivElement
|
||||
$stylePanel: HTMLDivElement
|
||||
$styleModes: HTMLDivElement
|
||||
$colors: HTMLDivElement
|
||||
$antiOverlap?: HTMLDivElement
|
||||
$syncVideo?: HTMLDivElement
|
||||
$opacitySlider: HTMLDivElement
|
||||
$opacityValue: HTMLDivElement
|
||||
$marginSlider: HTMLDivElement
|
||||
$marginValue: HTMLDivElement
|
||||
$fontSizeSlider: HTMLDivElement
|
||||
$fontSizeValue: HTMLDivElement
|
||||
$speedSlider: HTMLDivElement
|
||||
$speedValue: HTMLDivElement
|
||||
$input: HTMLInputElement
|
||||
$send: HTMLDivElement
|
||||
}
|
||||
|
||||
export interface SettingSlider { reset: (index?: number) => void }
|
||||
export interface SliderConfig {
|
||||
min: number
|
||||
max: number
|
||||
container: HTMLElement
|
||||
findIndex: () => number
|
||||
onChange: (index: number) => void
|
||||
steps?: SliderStep<unknown>[]
|
||||
}
|
||||
+66
-53
@@ -1,3 +1,6 @@
|
||||
import type Danmuku from './danmuku'
|
||||
import type { SettingSlider, SettingTemplate, SliderConfig } from './setting-types'
|
||||
import type { DanmukuArt, NormalizedOption, SliderOption } from './types'
|
||||
import SettingLifecycle from './setting-lifecycle'
|
||||
import { emitSetting, lockSetting, unlockSetting } from './setting-send'
|
||||
import createSlider from './setting-slider'
|
||||
@@ -5,7 +8,17 @@ import { settingIcons, settingTemplate } from './setting-template'
|
||||
import './setting-style'
|
||||
|
||||
export default class Setting {
|
||||
constructor(art, danmuku) {
|
||||
declare art: DanmukuArt
|
||||
declare danmuku: Danmuku
|
||||
declare utils: DanmukuArt['constructor']['utils']
|
||||
declare template: SettingTemplate
|
||||
declare slider: Record<'opacity' | 'margin' | 'fontSize' | 'speed', SettingSlider>
|
||||
declare emitting: boolean
|
||||
declare isLock: boolean
|
||||
declare timer: ReturnType<typeof setTimeout> | null
|
||||
declare lifecycle: SettingLifecycle
|
||||
|
||||
constructor(art: DanmukuArt, danmuku: Danmuku) {
|
||||
this.art = art
|
||||
this.danmuku = danmuku
|
||||
this.utils = art.constructor.utils
|
||||
@@ -16,32 +29,32 @@ export default class Setting {
|
||||
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,
|
||||
$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,
|
||||
opacity: null!,
|
||||
margin: null!,
|
||||
fontSize: null!,
|
||||
speed: null!,
|
||||
}
|
||||
|
||||
this.emitting = false
|
||||
@@ -56,7 +69,7 @@ export default class Setting {
|
||||
art.on('destroy', this.destroy)
|
||||
if (!this.lifecycle.active)
|
||||
return
|
||||
this.lifecycle.write($controlsCenter.style, 'display', 'flex', value => setStyle($controlsCenter, 'display', value))
|
||||
this.lifecycle.write($controlsCenter.style, 'display', 'flex', value => setStyle($controlsCenter, 'display', value as string))
|
||||
|
||||
this.createTemplate()
|
||||
if (!this.lifecycle.active)
|
||||
@@ -103,7 +116,7 @@ export default class Setting {
|
||||
return settingIcons()
|
||||
}
|
||||
|
||||
get option() {
|
||||
get option(): NormalizedOption {
|
||||
return this.danmuku.option
|
||||
}
|
||||
|
||||
@@ -115,7 +128,7 @@ export default class Setting {
|
||||
return settingTemplate.call(this)
|
||||
}
|
||||
|
||||
get OPACITY() {
|
||||
get OPACITY(): Required<SliderOption> {
|
||||
return {
|
||||
min: 0,
|
||||
max: 100,
|
||||
@@ -124,7 +137,7 @@ export default class Setting {
|
||||
}
|
||||
}
|
||||
|
||||
get FONT_SIZE() {
|
||||
get FONT_SIZE(): Required<SliderOption> {
|
||||
return {
|
||||
min: 12,
|
||||
max: 120,
|
||||
@@ -133,7 +146,7 @@ export default class Setting {
|
||||
}
|
||||
}
|
||||
|
||||
get MARGIN() {
|
||||
get MARGIN(): Required<SliderOption<NormalizedOption['margin']>> {
|
||||
return {
|
||||
min: 0,
|
||||
max: 3,
|
||||
@@ -159,7 +172,7 @@ export default class Setting {
|
||||
}
|
||||
}
|
||||
|
||||
get SPEED() {
|
||||
get SPEED(): Required<SliderOption> {
|
||||
return {
|
||||
min: 0,
|
||||
max: 4,
|
||||
@@ -212,13 +225,13 @@ export default class Setting {
|
||||
]
|
||||
}
|
||||
|
||||
query(selector) {
|
||||
query<T extends HTMLElement = HTMLDivElement>(selector: string): T {
|
||||
const { query } = this.utils
|
||||
const { $danmuku } = this.template
|
||||
return query(selector, $danmuku)
|
||||
return query(selector, $danmuku) as T
|
||||
}
|
||||
|
||||
append(el, target) {
|
||||
append(el: HTMLElement, target: HTMLElement) {
|
||||
if (!this.lifecycle.active)
|
||||
return
|
||||
const { append } = this.utils
|
||||
@@ -228,7 +241,7 @@ export default class Setting {
|
||||
append(el, target)
|
||||
}
|
||||
|
||||
setData(key, value) {
|
||||
setData(key: string, value: string | number | boolean) {
|
||||
if (!this.lifecycle.active)
|
||||
return
|
||||
const { $player } = this.art.template
|
||||
@@ -265,7 +278,7 @@ export default class Setting {
|
||||
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.$input = this.query<HTMLInputElement>('.apd-input')
|
||||
this.template.$send = this.query('.apd-send')
|
||||
|
||||
const { $toggle } = this.template
|
||||
@@ -290,7 +303,7 @@ export default class Setting {
|
||||
})
|
||||
|
||||
this.lifecycle.proxy($configModes, 'click', (event) => {
|
||||
const $mode = event.target.closest('.apd-mode')
|
||||
const $mode = (event.target as HTMLElement).closest<HTMLElement>('.apd-mode')
|
||||
if (!$mode)
|
||||
return
|
||||
const mode = Number($mode.dataset.mode)
|
||||
@@ -307,14 +320,14 @@ export default class Setting {
|
||||
this.reset()
|
||||
})
|
||||
|
||||
this.lifecycle.proxy($antiOverlap, 'click', () => {
|
||||
this.lifecycle.proxy($antiOverlap!, 'click', () => {
|
||||
this.danmuku.config({
|
||||
antiOverlap: !this.option.antiOverlap,
|
||||
})
|
||||
this.reset()
|
||||
})
|
||||
|
||||
this.lifecycle.proxy($syncVideo, 'click', () => {
|
||||
this.lifecycle.proxy($syncVideo!, 'click', () => {
|
||||
this.danmuku.config({
|
||||
synchronousPlayback: !this.option.synchronousPlayback,
|
||||
})
|
||||
@@ -322,7 +335,7 @@ export default class Setting {
|
||||
})
|
||||
|
||||
this.lifecycle.proxy($styleModes, 'click', (event) => {
|
||||
const $mode = event.target.closest('.apd-mode')
|
||||
const $mode = (event.target as HTMLElement).closest<HTMLElement>('.apd-mode')
|
||||
if (!$mode)
|
||||
return
|
||||
const mode = Number($mode.dataset.mode)
|
||||
@@ -333,7 +346,7 @@ export default class Setting {
|
||||
})
|
||||
|
||||
this.lifecycle.proxy($colors, 'click', (event) => {
|
||||
const $color = event.target.closest('.apd-color')
|
||||
const $color = (event.target as HTMLElement).closest<HTMLElement>('.apd-color')
|
||||
if (!$color)
|
||||
return
|
||||
this.danmuku.config({
|
||||
@@ -373,7 +386,7 @@ export default class Setting {
|
||||
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],
|
||||
item => item.value![0] === this.option.margin[0] && item.value![1] === this.option.margin[1],
|
||||
)
|
||||
},
|
||||
onChange: (index) => {
|
||||
@@ -381,7 +394,7 @@ export default class Setting {
|
||||
if (!margin)
|
||||
return
|
||||
const { $marginValue } = this.template
|
||||
$marginValue.textContent = margin.name
|
||||
$marginValue.textContent = margin.name as string
|
||||
this.danmuku.config({
|
||||
margin: margin.value,
|
||||
})
|
||||
@@ -416,7 +429,7 @@ export default class Setting {
|
||||
if (!speed)
|
||||
return
|
||||
const { $speedValue } = this.template
|
||||
$speedValue.textContent = speed.name
|
||||
$speedValue.textContent = speed.name as string
|
||||
this.danmuku.config({
|
||||
speed: speed.value,
|
||||
})
|
||||
@@ -424,11 +437,11 @@ export default class Setting {
|
||||
})
|
||||
}
|
||||
|
||||
createSlider(option) {
|
||||
createSlider(option: SliderConfig) {
|
||||
return createSlider.call(this, option)
|
||||
}
|
||||
|
||||
onFullscreen(state) {
|
||||
onFullscreen(state: boolean) {
|
||||
if (!this.lifecycle.active)
|
||||
return
|
||||
const { $danmuku, $controlsCenter, $mount } = this.template
|
||||
@@ -445,7 +458,7 @@ export default class Setting {
|
||||
}
|
||||
}
|
||||
|
||||
onMouseEnter({ $control, $panel }) {
|
||||
onMouseEnter({ $control, $panel }: { $control: HTMLElement, $panel: HTMLElement }) {
|
||||
if (!this.lifecycle.active)
|
||||
return
|
||||
const { $player } = this.art.template
|
||||
@@ -522,7 +535,7 @@ export default class Setting {
|
||||
this.setData('danmukuTheme', this.option.theme)
|
||||
this.setData('danmukuEmitter', this.option.emitter)
|
||||
|
||||
const colors = $colors.children
|
||||
const colors = $colors.children as HTMLCollectionOf<HTMLElement>
|
||||
const $color = Array.from(colors).find(item => item.dataset.color === this.option.color.toUpperCase())
|
||||
$color && inverseClass($color, 'apd-active')
|
||||
|
||||
@@ -531,14 +544,14 @@ export default class Setting {
|
||||
this.resize()
|
||||
}
|
||||
|
||||
mount(target) {
|
||||
mount(target: NormalizedOption['mount']) {
|
||||
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
|
||||
const $el = typeof target === 'string' ? document.querySelector<HTMLElement>(target) : target
|
||||
errorHandle($el as unknown as boolean, `Can not find the mount point: ${target}`)
|
||||
this.append($el!, this.template.$danmuku)
|
||||
this.template.$mount = $el!
|
||||
this.reset()
|
||||
}
|
||||
|
||||
@@ -546,7 +559,7 @@ export default class Setting {
|
||||
if (this.lifecycle.closed)
|
||||
return
|
||||
this.lifecycle.close()
|
||||
clearTimeout(this.timer)
|
||||
clearTimeout(this.timer as number | undefined)
|
||||
this.timer = null
|
||||
this.emitting = false
|
||||
this.isLock = false
|
||||
@@ -0,0 +1,19 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
|
||||
// Public runtime data has one declaration source; host and Worker adapters stay internal.
|
||||
export type {
|
||||
Danmu as DanmuInput,
|
||||
Item as DanmuItem,
|
||||
Input as DanmukuInput,
|
||||
RuntimeOption as DanmukuOption,
|
||||
State as DanmuState,
|
||||
Heatmap as HeatmapOptions,
|
||||
Point as HeatmapPoint,
|
||||
NormalizedDanmu,
|
||||
NormalizedOption,
|
||||
Slider as SliderOption,
|
||||
SliderStep,
|
||||
} from '../types/runtime-shared'
|
||||
|
||||
export type DanmukuArt = Artplayer & { constructor: typeof Artplayer, isRotate?: boolean }
|
||||
export type DanmuStyle = Partial<CSSStyleDeclaration>
|
||||
@@ -0,0 +1,4 @@
|
||||
declare module '*?worker&inline' {
|
||||
const WorkerConstructor: new (options?: WorkerOptions) => Worker
|
||||
export default WorkerConstructor
|
||||
}
|
||||
+16
-8
@@ -1,14 +1,22 @@
|
||||
import type { PlacementReply, PlacementRequest } from './worker-types'
|
||||
|
||||
let nextRequest = 0
|
||||
|
||||
export default class WorkerClient {
|
||||
constructor(createWorker, onFailure) {
|
||||
declare pending: Map<number, { resolve: (value: PlacementReply) => void, reject: (error: unknown) => void }>
|
||||
declare closed: boolean
|
||||
declare failed: boolean
|
||||
declare onFailure: (error: unknown) => void
|
||||
declare worker: Worker
|
||||
|
||||
constructor(createWorker: () => Worker, onFailure: (error: unknown) => void) {
|
||||
this.pending = new Map()
|
||||
this.closed = false
|
||||
this.failed = false
|
||||
this.onFailure = onFailure
|
||||
this.worker = createWorker()
|
||||
try {
|
||||
this.worker.onmessage = (event) => {
|
||||
this.worker.onmessage = (event: MessageEvent<PlacementReply>) => {
|
||||
const request = this.pending.get(event.data?.id)
|
||||
if (!request)
|
||||
return
|
||||
@@ -19,7 +27,7 @@ export default class WorkerClient {
|
||||
event.preventDefault?.()
|
||||
this.fail(event.error || event)
|
||||
}
|
||||
this.worker.onmessageerror = event => this.fail(event.error || event)
|
||||
this.worker.onmessageerror = event => this.fail((event as MessageEvent & { error?: unknown }).error || event)
|
||||
}
|
||||
catch (error) {
|
||||
try {
|
||||
@@ -30,7 +38,7 @@ export default class WorkerClient {
|
||||
}
|
||||
}
|
||||
|
||||
request(message) {
|
||||
request(message: PlacementRequest): Promise<PlacementReply> {
|
||||
message.id = ++nextRequest
|
||||
const { id } = message
|
||||
if (this.closed || this.failed)
|
||||
@@ -46,7 +54,7 @@ export default class WorkerClient {
|
||||
})
|
||||
}
|
||||
|
||||
fail(error) {
|
||||
fail(error: unknown) {
|
||||
if (this.closed || this.failed)
|
||||
return
|
||||
this.failed = true
|
||||
@@ -70,8 +78,8 @@ export default class WorkerClient {
|
||||
this.closed = true
|
||||
this.cancel()
|
||||
let failed = false
|
||||
let error
|
||||
const attempt = (callback) => {
|
||||
let error: unknown
|
||||
const attempt = (callback: () => void) => {
|
||||
try {
|
||||
callback()
|
||||
}
|
||||
@@ -81,7 +89,7 @@ export default class WorkerClient {
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
for (const name of ['onmessage', 'onerror', 'onmessageerror']) {
|
||||
for (const name of ['onmessage', 'onerror', 'onmessageerror'] as const) {
|
||||
attempt(() => this.worker[name] = null)
|
||||
}
|
||||
attempt(() => this.worker.terminate())
|
||||
@@ -0,0 +1,32 @@
|
||||
export interface VisibleDanmu {
|
||||
mode: number
|
||||
top: number
|
||||
left: number
|
||||
right: number
|
||||
height: number
|
||||
width: number
|
||||
speed: number
|
||||
distance: number
|
||||
time: number
|
||||
}
|
||||
|
||||
export interface PlacementRequest {
|
||||
id?: number
|
||||
type: 'getDanmuTop'
|
||||
target: { mode: number, height: number, speed: number }
|
||||
visibles: VisibleDanmu[]
|
||||
clientWidth: number
|
||||
clientHeight: number
|
||||
marginBottom: number
|
||||
marginTop: number
|
||||
antiOverlap: boolean
|
||||
}
|
||||
|
||||
export interface PlacementMessage extends PlacementRequest { id: number }
|
||||
export interface PlacementReply { id: number, result: number | undefined }
|
||||
|
||||
export interface PlacementBoundary extends Omit<VisibleDanmu, 'mode' | 'time'> {
|
||||
type: 'top' | 'bottom'
|
||||
}
|
||||
|
||||
export type PlacementRow = VisibleDanmu | PlacementBoundary
|
||||
+16
-13
@@ -1,9 +1,11 @@
|
||||
function getDanmuTop({ target, visibles, clientWidth, clientHeight, marginBottom, marginTop, antiOverlap }) {
|
||||
import type { PlacementMessage, PlacementReply, PlacementRequest, PlacementRow, VisibleDanmu } from './worker-types'
|
||||
|
||||
function getDanmuTop({ target, visibles, clientWidth, clientHeight, marginBottom, marginTop, antiOverlap }: PlacementRequest): number | undefined {
|
||||
// 弹幕最大高度
|
||||
const maxTop = clientHeight - marginBottom
|
||||
|
||||
// 过滤同模式的弹幕,即每种模式各不影响
|
||||
const danmus = visibles
|
||||
const danmus: PlacementRow[] = visibles
|
||||
.filter(item => item.mode === target.mode && item.top <= maxTop)
|
||||
.sort((prev, next) => prev.top - next.top)
|
||||
|
||||
@@ -44,8 +46,8 @@ function getDanmuTop({ target, visibles, clientWidth, clientHeight, marginBottom
|
||||
if (target.mode === 2) {
|
||||
// 倒序查找
|
||||
for (let index = danmus.length - 2; index >= 0; index -= 1) {
|
||||
const item = danmus[index]
|
||||
const prev = danmus[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) {
|
||||
@@ -56,8 +58,8 @@ function getDanmuTop({ target, visibles, clientWidth, clientHeight, marginBottom
|
||||
else {
|
||||
// 顺序查找
|
||||
for (let index = 1; index < danmus.length; index += 1) {
|
||||
const item = danmus[index]
|
||||
const prev = danmus[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) {
|
||||
@@ -66,12 +68,13 @@ function getDanmuTop({ target, visibles, clientWidth, clientHeight, marginBottom
|
||||
}
|
||||
}
|
||||
|
||||
const topMap = []
|
||||
const topMap: VisibleDanmu[][] = []
|
||||
for (let index = 1; index < danmus.length - 1; index += 1) {
|
||||
const item = danmus[index]
|
||||
// The loop excludes the two virtual boundary rows inserted above.
|
||||
const item = danmus[index] as VisibleDanmu
|
||||
if (topMap.length) {
|
||||
const last = topMap[topMap.length - 1]
|
||||
if (last[0].top === item.top) {
|
||||
const last = topMap[topMap.length - 1]!
|
||||
if (last[0]!.top === item.top) {
|
||||
last.push(item)
|
||||
}
|
||||
else {
|
||||
@@ -131,11 +134,11 @@ function getDanmuTop({ target, visibles, clientWidth, clientHeight, marginBottom
|
||||
break
|
||||
}
|
||||
|
||||
return topMap[0][0].top
|
||||
return topMap[0]![0]!.top
|
||||
}
|
||||
}
|
||||
|
||||
onmessage = (event) => {
|
||||
onmessage = (event: MessageEvent<PlacementMessage>) => {
|
||||
const { data } = event
|
||||
if (!data.id || !data.type)
|
||||
return
|
||||
@@ -147,5 +150,5 @@ onmessage = (event) => {
|
||||
globalThis.postMessage({
|
||||
result,
|
||||
id: data.id,
|
||||
})
|
||||
} satisfies PlacementReply)
|
||||
}
|
||||
Reference in new issue
Block a user