mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-26 00: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
@@ -2,4 +2,5 @@ node_modules
|
||||
.DS_Store
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
src
|
||||
src
|
||||
tsconfig.json
|
||||
@@ -2,40 +2,100 @@
|
||||
|
||||
The compatibility reference is the actual npm 5.3.0 package. Its archive,
|
||||
historical declarations, earlier exports and failure evidence are indexed in
|
||||
`refactor/baselines/danmuku-release.json` and `danmuku-contract.md` at the
|
||||
repository root. Do not infer runtime contracts from the old declaration file.
|
||||
`refactor/baselines/danmuku-release.json` and
|
||||
`refactor/baselines/danmuku-contract.md` relative to the repository root. Do not infer runtime contracts from the old declaration file.
|
||||
|
||||
## Module responsibilities
|
||||
|
||||
| Module | Responsibility |
|
||||
| --- | --- |
|
||||
| `src/index.js` | Plugin factory, facade getters, bound methods, setting and heatmap composition |
|
||||
| `src/danmuku.js` | Internal instance, method return identity, configuration/load commits, event and lifecycle wiring |
|
||||
| `src/config.js` | Fresh defaults, validation schema, configuration comparison and normalization |
|
||||
| `src/input.js` | Input forms, replacement ownership, independent append operations, cancellation and synchronous callback reentry |
|
||||
| `src/bilibili-parser.js` | Pure legacy-compatible XML fields, mode mapping and entity decoding |
|
||||
| `src/bilibili.js` | Fetch and response text, one parser Worker per request, fallback, settlement and Blob URL cleanup |
|
||||
| `src/scheduler.js` | One RAF or asynchronous frame, cancellation generations, preparation ownership and failure recovery |
|
||||
| `src/renderer.js` | Owned node allocation, preparation, geometry snapshots, placement, pause/resume styling and disposal |
|
||||
| `src/queue.js` | Ordered state pools, eligibility and state transitions |
|
||||
| `src/worker-client.js` | Track Worker lifecycle, a shared response dispatcher, unique request IDs and pending requests |
|
||||
| `src/worker.js` | Track placement Worker; a separate protocol from the XML parser Worker |
|
||||
| `src/setting.js` | Settings coordinator, configuration controls, mount and fullscreen layout |
|
||||
| `src/setting-template.js` | Existing template HTML and static icon values |
|
||||
| `src/setting-slider.js` | Slider index, pointer and rotation behavior |
|
||||
| `src/setting-send.js` | Pending send, original error outlet and lock countdown |
|
||||
| `src/setting-lifecycle.js` | Exact subscription/proxy disposal, cancellation and conditional host-property restoration |
|
||||
| `src/setting-style.js` | Shared document style and one pending DOMContentLoaded installation |
|
||||
| `src/heatmap.js` | Owned control, progress stops, subscriptions and per-instance gradient |
|
||||
| `src/heatmap-sampling.js` | Sorted-time bin counts with historical numeric boundaries |
|
||||
| `src/heatmap-geometry.js` | Legacy point normalization, caller-visible point writes and SVG curve calculation |
|
||||
| `src/index.ts` | Plugin factory, facade getters, bound methods, setting and heatmap composition |
|
||||
| `src/danmuku.ts` | Internal instance, method return identity, configuration/load commits, event and lifecycle wiring |
|
||||
| `src/config.ts` | Fresh defaults, validation schema, configuration comparison and normalization |
|
||||
| `src/input.ts` | Input forms, replacement ownership, independent append operations, cancellation and synchronous callback reentry |
|
||||
| `src/bilibili-parser.ts` | Pure legacy-compatible XML fields, mode mapping and entity decoding |
|
||||
| `src/bilibili.ts` | Fetch and response text, one parser Worker per request, fallback, settlement and Blob URL cleanup |
|
||||
| `src/scheduler.ts` | One RAF or asynchronous frame, cancellation generations, preparation ownership and failure recovery |
|
||||
| `src/renderer.ts` | Owned node allocation, preparation, geometry snapshots, placement, pause/resume styling and disposal |
|
||||
| `src/queue.ts` | Ordered state pools, eligibility and state transitions |
|
||||
| `src/worker-client.ts` | Track Worker lifecycle, a shared response dispatcher, unique request IDs and pending requests |
|
||||
| `src/worker.ts` | Track placement Worker; a separate protocol from the XML parser Worker |
|
||||
| `src/setting.ts` | Settings coordinator, configuration controls, mount and fullscreen layout |
|
||||
| `src/setting-template.ts` | Existing template HTML and static icon values |
|
||||
| `src/setting-slider.ts` | Slider index, pointer and rotation behavior |
|
||||
| `src/setting-send.ts` | Pending send, original error outlet and lock countdown |
|
||||
| `src/setting-lifecycle.ts` | Exact subscription/proxy disposal, cancellation and conditional host-property restoration |
|
||||
| `src/setting-style.ts` | Shared document style and one pending DOMContentLoaded installation |
|
||||
| `src/heatmap.ts` | Owned control, progress stops, subscriptions and per-instance gradient |
|
||||
| `src/heatmap-sampling.ts` | Sorted-time bin counts with historical numeric boundaries |
|
||||
| `src/heatmap-geometry.ts` | Legacy point normalization, caller-visible point writes and SVG curve calculation |
|
||||
|
||||
The package is undergoing staged migration. PKG-DANMUKU-03 separates input and
|
||||
configuration responsibilities in JavaScript; PKG-DANMUKU-04 separates scheduling
|
||||
and track requests; PKG-DANMUKU-05 separates rendering and UI resources;
|
||||
PKG-DANMUKU-06 converts all owned
|
||||
modules and Worker messages to TypeScript and validates installed consumers.
|
||||
This intermediate structure is not the final TypeScript or release acceptance.
|
||||
All 20 owned runtime modules use TypeScript. Their responsibilities remain
|
||||
separate from public declaration compatibility and from release acceptance.
|
||||
The following files contain types only:
|
||||
|
||||
| Module | Responsibility |
|
||||
| --- | --- |
|
||||
| `src/types.ts` | Reexports public runtime data types and defines the internal Artplayer host adapter |
|
||||
| `src/worker-types.ts` | Placement requests, replies, visible rows and virtual boundary rows |
|
||||
| `src/parser-types.ts` | XML parser results and its independent Worker protocol |
|
||||
| `src/setting-types.ts` | Settings template slots and slider integration contracts |
|
||||
| `src/worker-assets.d.ts` | Inline placement Worker module declaration |
|
||||
| `types/runtime-shared.d.ts` | Single source for accurate option, callback, input, queue, facade, owner and event payload types |
|
||||
|
||||
`src/types.ts` imports/reexports `types/runtime-shared.d.ts` with type-only
|
||||
syntax. Keep shared option/data definitions there; do not create a second copy
|
||||
in production source. Host, DOM and Worker adapter types remain internal. The
|
||||
factory composes Danmuku, Setting and heatmap. Danmuku owns queue/state pools and
|
||||
coordinates input, renderer and scheduler. Scheduler/renderer refer back to the
|
||||
owner with type-only imports; these references add no runtime module cycle.
|
||||
The XML parser and heatmap sampling/geometry remain independently testable.
|
||||
|
||||
## Declaration entrypoints and type boundaries
|
||||
|
||||
The root and `/legacy` declarations retain the exact actual npm 5.3.0 type
|
||||
source, including historical inaccuracies and extraction behavior. Edit neither
|
||||
to make runtime implementation checks pass. The additive `/runtime` entrypoint
|
||||
selects the existing ESM or CommonJS factory with accurate declarations. Its
|
||||
`runtime.d.mts` entry exposes ESM type exports; `runtime.d.cts` and the
|
||||
`typesVersions` fallback `runtime.d.ts` expose the callable CommonJS factory
|
||||
and its type namespace. CommonJS returns the function itself; importing it does
|
||||
not add a `factory.default` property.
|
||||
|
||||
The argument object is required, while its fields are optional. The registrar
|
||||
returns `RuntimeResult` synchronously. Command methods resolve/return `Owner`,
|
||||
which is a different object; `emit` is asynchronous even though its initial
|
||||
insertion work is synchronous. The facade retains its three live getters and
|
||||
requires a valid target for `mount`. Public data includes mutable point tuples,
|
||||
typed static icons, normalized filter inputs and queue entries for visibility
|
||||
callbacks. Filters and visibility callbacks accept truthy results; sending
|
||||
requires strict `true`. Their `this` is the current normalized option. Loader
|
||||
functions retain their receiver-free call. `EventMap` supplies named payload
|
||||
tuples explicitly; importing `/runtime` does not augment old core event types.
|
||||
It contains types only, with no new event bus or runtime export.
|
||||
|
||||
Keep these narrow implementation assertions documented when editing them:
|
||||
|
||||
- Native style/dataset writes retain their historical numeric and null values.
|
||||
WebIDL performs the conversion; changing source assignments to satisfy DOM
|
||||
declarations could change controlled hosts or observable setters.
|
||||
- Emit/stop state pools own allocated nodes. A frame with a non-null reference
|
||||
has its associated item, and release checks node identity. Non-null assertions
|
||||
depend on those invariants; they do not replace lifecycle validation.
|
||||
- The legacy public core type describes `constructor` as `Function`. The
|
||||
factory's host adapter narrows it to the existing Artplayer static utilities
|
||||
and validator. It introduces no new core capability or minimum core version.
|
||||
- An omitted internal `postMessage` keeps its old empty-message behavior;
|
||||
unsupported margin strings retain the raw historical fallback at the Worker
|
||||
boundary. Placement types describe valid scheduler requests, with explicit
|
||||
compatibility assertions for those exceptional paths. These internal message
|
||||
helpers are not additional methods promised by the public `Owner` interface.
|
||||
|
||||
The strict package config disables JavaScript input. Its `ES2021.String` type
|
||||
library describes the existing XML `replaceAll` calls; it neither introduces a
|
||||
new runtime dependency nor raises the existing browser requirement. Modern and
|
||||
legacy build targets remain es2020 and es2015; syntax lowering does not polyfill
|
||||
`replaceAll` or other browser APIs.
|
||||
|
||||
## Contracts to retain
|
||||
|
||||
@@ -125,6 +185,25 @@ Late, unknown and duplicate replies do not settle a different request.
|
||||
|
||||
## Verification and remaining work
|
||||
|
||||
Run package commands from the repository root with the pinned Node version and
|
||||
Yarn Classic 1.22.22:
|
||||
|
||||
| Command | What it checks or produces |
|
||||
| --- | --- |
|
||||
| `yarn test:danmuku` | Historical and candidate input, parser, Worker, scheduling, rendering, settings and heatmap regressions |
|
||||
| `yarn test:danmuku-types` | Frozen root declarations, current/legacy compiler consumers, negative cases, implementation assignability and editor types |
|
||||
| `yarn test:danmuku-types-package` | Packed isolated installs, real entrypoints and consumer module/compiler modes; creates local evidence, does not publish |
|
||||
| `yarn typecheck` | Strict production sources and the repository consumer type matrix |
|
||||
| `yarn build artplayer-plugin-danmuku` | Normal package outputs and copied docs artifacts, including the inline TS Worker |
|
||||
| `yarn dev artplayer-plugin-danmuku` | Local demo rebuild for the package |
|
||||
|
||||
Edit `src/` for runtime changes, `types/runtime-shared.d.ts` for shared accurate
|
||||
data contracts and the corresponding `runtime.d.*` entry for module shape.
|
||||
Rebuild artifacts through the package script. The frozen root declaration and
|
||||
historical test helpers are compatibility evidence, not generated repair targets.
|
||||
Use `refactor/fixtures/implementation/danmuku.ts` when a source/declaration change must
|
||||
prove assignability to the actual implementation.
|
||||
|
||||
`yarn test:danmuku` runs frozen historical contracts/failures and candidate input,
|
||||
parser, scheduler and Worker regressions. The historical helper and archives must remain frozen;
|
||||
candidate tests use `test/helpers/danmuku-candidate.js` and may select built
|
||||
@@ -143,9 +222,11 @@ three modes, strict dense geometry, one request per prepared item, native
|
||||
pause/seek/rate and visible-node reuse. Preserve both geometric and protocol
|
||||
assertions; unique IDs alone do not prove non-overlapping tracks.
|
||||
|
||||
Long-run native load behavior still requires PKG-DANMUKU-07. Check task evidence
|
||||
and the risk register before changing completion or release status; short native
|
||||
regressions are not package-wide stability acceptance.
|
||||
Long-run native load behavior still requires PKG-DANMUKU-07; cross-plugin/core
|
||||
combination coverage and final distribution acceptance remain PKG-DANMUKU-08/09.
|
||||
Check task evidence and the risk register before changing completion or release
|
||||
status. These commands and short native regressions do not establish npm release
|
||||
readiness; final checks and release reviews remain separate gates.
|
||||
|
||||
## Rendering and UI resources
|
||||
|
||||
|
||||
@@ -6,6 +6,38 @@ Danmuku plugin for ArtPlayer
|
||||
|
||||
[https://artplayer.org](https://artplayer.org/?libs=./uncompiled/artplayer-plugin-danmuku/index.js&example=danmuku)
|
||||
|
||||
## TypeScript entrypoints
|
||||
|
||||
Existing imports from `artplayer-plugin-danmuku` and `/legacy` retain the npm
|
||||
5.3.0 declarations. Use `/runtime` for declarations that describe the actual
|
||||
factory, asynchronous commands and returned internal owner:
|
||||
|
||||
```ts
|
||||
import danmuku from 'artplayer-plugin-danmuku/runtime'
|
||||
import type { Point, RuntimeOption } from 'artplayer-plugin-danmuku/runtime'
|
||||
|
||||
const option: RuntimeOption = { danmuku: [], heatmap: true }
|
||||
const points: Point[] = [[0, 10], [200, 20]]
|
||||
```
|
||||
|
||||
The option object is required; each field is optional. Registration returns the
|
||||
plugin facade immediately. `await plugin.emit(row)` and `await plugin.load()`
|
||||
return the internal owner, as do synchronous `config/hide/show/reset` commands.
|
||||
That owner differs from the registered facade. `mount(target)` returns undefined
|
||||
and requires a valid element or selector. The facade's option/state getters
|
||||
remain live.
|
||||
|
||||
The accurate entrypoint also exposes icon types, callback receivers and an
|
||||
explicit `EventMap` of payload tuples. Filters and visibility callbacks receive
|
||||
the current option as `this`; `beforeEmit` does too and accepts only strict
|
||||
`true`. Importing these types does not automatically alter Artplayer's historical
|
||||
event declarations. Custom points use mutable `[number, number]` tuples and
|
||||
retain the existing inner-array modifications during rendering.
|
||||
|
||||
ESM imports select the existing ESM factory; CommonJS
|
||||
`require('artplayer-plugin-danmuku/runtime')` returns the existing callable
|
||||
factory directly. No additional runtime implementation is loaded.
|
||||
|
||||
## Loading and configuration
|
||||
|
||||
`plugin.load()` reloads the configured input and replaces the queue after input
|
||||
@@ -66,7 +98,10 @@ finite `heatmap.yMin` or `heatmap.yMax`, and custom points events, retain their
|
||||
original coordinate mapping; use these when deliberately controlling the scale.
|
||||
|
||||
See [ARCHITECTURE.md](./ARCHITECTURE.md) for module ownership, compatibility
|
||||
constraints, tests and the remaining staged TypeScript migration.
|
||||
constraints and editing/verification commands. All owned runtime modules now use
|
||||
TypeScript, with shared public runtime data types maintained in
|
||||
`types/runtime-shared.d.ts`. Long-duration load tests, combination/distribution
|
||||
acceptance and release reviews remain separate unfinished stages.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDanmuku=e(),define(function(){return t.artplayerPluginDanmuku})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDanmuku=e()}(this,function(){"use strict";function t(t){return t instanceof Promise||"[object Promise]"===Object.prototype.toString.call(t)}function e(t){switch(t){case 1:case 2:case 3:default:return 0;case 4:return 2;case 5:return 1}}function i(t){if("string"!==typeof t)return[];const i=t.matchAll(/<d[^>]*?p="(?<p>[^"]+)"[^>]*>(?<text>.*?)<\/d>/gs);return Array.from(i).map(t=>{const i=t.groups.p.split(",");if(i.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(i[0]),mode:e(Number(i[1])),fontSize:Number(i[2]),color:`#${Number(i[3]).toString(16)}`,timestamp:Number(i[4]),pool:Number(i[5]),userID:i[6],rowID:Number(i[7])}}return null}).filter(Boolean)}function n({data:t}){const{xml:e,id:n}=t,a=i(e);globalThis.postMessage({danmus:a,id:n})}let a=0;function s(t,{signal:s,onCancel:r}={}){return new Promise((o,l)=>{let d,c,p,h=!1;const u=++a,m=()=>g(void 0,function(t){if(void 0!==t?.reason)return t.reason;const e=new Error("Bilibili Danmu loading cancelled");return e.name="AbortError",e}(s));function f(){let t,e=!1;try{if(d){const t=d;d=void 0,t.onmessage=null,t.onerror=null,t.onmessageerror=null,t.terminate()}}catch(i){t=i,e=!0}if(void 0!==c){const n=c;c=void 0;try{URL.revokeObjectURL(n)}catch(i){e||(t=i),e=!0}}if(e)throw t}function g(t,e){if(h)return;h=!0;let i=arguments.length>1;s?.removeEventListener("abort",m),p?.();try{f()}catch(n){i||(e=n),i=!0}i?l(e):o(t)}function y(t,e){if(!h){console.error("Error parsing Bilibili Danmu:",e);try{g(i(t))}catch(n){g(void 0,n)}}}s?.aborted?m():(s?.addEventListener("abort",m),p=r?.(m),h?p?.():async function(){const a=s?await fetch(t,{signal:s}):await fetch(t);if(h)return;const r=await a.text();if(!h)try{const t=`\n ${e.toString()}\n ${i.toString()}\n onmessage = ${n.toString()}\n `;if(c=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),h)return void f();if(d=new Worker(c),h)return void f();d.onmessage=t=>{if(h||t.data?.id!==u)return;const{danmus:e}=t.data;Array.isArray(e)?g(e):g(void 0,new Error("Invalid Bilibili Danmu worker response"))},d.onerror=t=>{t.preventDefault?.(),y(r,t.error||t)},d.onmessageerror=t=>y(r,t.error||t),d.postMessage({xml:r,id:u})}catch(o){y(r,o)}}().catch(t=>g(void 0,t)))})}const r=new WeakMap,o=Symbol("cancelled danmuku input");function l(t){return r.has(t)||r.set(t,{closed:!1,tasks:new Set,emitting:[]}),r.get(t)}function d(t){const e=l(t),i=e.emitting[e.emitting.length-1];return!e.closed&&!t.art.isDestroy&&(!i||i.active())}function c(t){const e=l(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}function p(t,e,i){const n=t.states[e]||[];for(let a=0;a<n.length;a++)i(n[a]);return n}class h{constructor(t){this.owner=t,this.nodes=new Set}acquire(){const t=this.owner.$refs.pop()||document.createElement("div");return t.style.cssText=this.owner.constructor.cssText,t.dataset.mode="",t.dataset.id="",t.className="",this.nodes.add(t),t}get visibles(){const t=[],{clientWidth:e}=this.owner.$player,i=this.owner.getLeft(this.owner.$player);return this.owner.filter("emit",n=>{const a=n.$ref.offsetTop,s=this.owner.getLeft(n.$ref)-i,r=n.$ref.clientHeight,o=n.$ref.clientWidth,l=s+o,d=e-l,c=l/n.$restTime,p={};p.top=a,p.left=s,p.height=r,p.width=o,p.right=d,p.speed=c,p.distance=l,p.time=n.$restTime,p.mode=n.mode,t.push(p)}),t}left(t){const e=t.getBoundingClientRect();return this.owner.isRotate?e.top:e.left}makeWait(t){this.owner.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=this.owner.constructor.cssText,t.$ref.style.visibility="hidden",t.$ref.style.marginLeft="0px",t.$ref.style.transform="translateX(0px)",t.$ref.style.transition="transform 0s linear 0s",this.owner.$refs.push(t.$ref),t.$ref=null)}resize(){const{clientWidth:t}=this.owner.$player;this.owner.filter("stop",e=>{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.owner.filter("emit",e=>{switch(e.$lastStartTime=Date.now(),e.mode){case 0:{const i=t+e.$ref.clientWidth;e.$ref.style.left=`${t}px`,e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition=`transform ${e.$restTime}s linear 0s`;break}}})}continue(){const{clientWidth:t}=this.owner.$player;return this.owner.filter("stop",e=>{switch(this.owner.setState(e,"emit"),e.$lastStartTime=Date.now(),e.mode){case 0:{const i=t+e.$ref.clientWidth;e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition=`transform ${e.$restTime}s linear 0s`;break}}}),this}suspend(){const{clientWidth:t}=this.owner.$player;return this.owner.filter("emit",e=>{switch(this.owner.setState(e,"stop"),e.mode){case 0:{const i=t-(this.owner.getLeft(e.$ref)-this.owner.getLeft(this.owner.$player));e.$ref.style.transform=`translLine truncated
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDanmuku=e(),define(function(){return t.artplayerPluginDanmuku})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDanmuku=e()}(this,function(){"use strict";function t(t){return t instanceof Promise||"[object Promise]"===Object.prototype.toString.call(t)}function e(t){switch(t){case 1:case 2:case 3:default:return 0;case 4:return 2;case 5:return 1}}function i(t){if("string"!==typeof t)return[];const i=t.matchAll(/<d[^>]*?p="(?<p>[^"]+)"[^>]*>(?<text>.*?)<\/d>/gs);return Array.from(i).map(t=>{const i=t.groups.p.split(",");if(i.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(i[0]),mode:e(Number(i[1])),fontSize:Number(i[2]),color:`#${Number(i[3]).toString(16)}`,timestamp:Number(i[4]),pool:Number(i[5]),userID:i[6],rowID:Number(i[7])}}return null}).filter(Boolean)}function n({data:t}){const{xml:e,id:n}=t,a=i(e);globalThis.postMessage({danmus:a,id:n})}let a=0;function s(t,{signal:s,onCancel:r}={}){return new Promise((o,l)=>{let d,c,p,h=!1;const u=++a,m=()=>g(void 0,function(t){if(void 0!==t?.reason)return t.reason;const e=new Error("Bilibili Danmu loading cancelled");return e.name="AbortError",e}(s));function f(){let t,e=!1;try{if(d){const t=d;d=void 0,t.onmessage=null,t.onerror=null,t.onmessageerror=null,t.terminate()}}catch(i){t=i,e=!0}if(void 0!==c){const n=c;c=void 0;try{URL.revokeObjectURL(n)}catch(i){e||(t=i),e=!0}}if(e)throw t}function g(t,e){if(h)return;h=!0;let i=arguments.length>1;s?.removeEventListener("abort",m),p?.();try{f()}catch(n){i||(e=n),i=!0}i?l(e):o(t)}function y(t,e){if(!h){console.error("Error parsing Bilibili Danmu:",e);try{g(i(t))}catch(n){g(void 0,n)}}}s?.aborted?m():(s?.addEventListener("abort",m),p=r?.(m),h?p?.():async function(){const a=s?await fetch(t,{signal:s}):await fetch(t);if(h)return;const r=await a.text();if(!h)try{const t=`\n ${e.toString()}\n ${i.toString()}\n onmessage = ${n.toString()}\n `;if(c=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),h)return void f();if(d=new Worker(c),h)return void f();d.onmessage=t=>{if(h||t.data?.id!==u)return;const{danmus:e}=t.data;Array.isArray(e)?g(e):g(void 0,new Error("Invalid Bilibili Danmu worker response"))},d.onerror=t=>{t.preventDefault?.(),y(r,t.error||t)},d.onmessageerror=t=>y(r,t.error||t),d.postMessage({xml:r,id:u})}catch(o){y(r,o)}}().catch(t=>g(void 0,t)))})}const r=new WeakMap,o=Symbol("cancelled danmuku input");function l(t){return r.has(t)||r.set(t,{closed:!1,tasks:new Set,emitting:[]}),r.get(t)}function d(t){const e=l(t),i=e.emitting[e.emitting.length-1];return!e.closed&&!t.art.isDestroy&&(!i||i.active())}function c(t){const e=l(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}function p(t,e,i){const n=t.states[e]||[];for(let a=0;a<n.length;a++)i(n[a]);return n}class h{constructor(t){this.owner=t,this.nodes=new Set}acquire(){const t=this.owner.$refs.pop()||document.createElement("div");return t.style.cssText=this.owner.constructor.cssText,t.dataset.mode="",t.dataset.id="",t.className="",this.nodes.add(t),t}get visibles(){const t=[],{clientWidth:e}=this.owner.$player,i=this.owner.getLeft(this.owner.$player);return this.owner.filter("emit",n=>{const a=n.$ref.offsetTop,s=this.owner.getLeft(n.$ref)-i,r=n.$ref.clientHeight,o=n.$ref.clientWidth,l=s+o,d=e-l,c=l/n.$restTime,p={};p.top=a,p.left=s,p.height=r,p.width=o,p.right=d,p.speed=c,p.distance=l,p.time=n.$restTime,p.mode=n.mode,t.push(p)}),t}left(t){const e=t.getBoundingClientRect();return this.owner.isRotate?e.top:e.left}makeWait(t){this.owner.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=this.owner.constructor.cssText,t.$ref.style.visibility="hidden",t.$ref.style.marginLeft="0px",t.$ref.style.transform="translateX(0px)",t.$ref.style.transition="transform 0s linear 0s",this.owner.$refs.push(t.$ref),t.$ref=null)}resize(){const{clientWidth:t}=this.owner.$player;this.owner.filter("stop",e=>{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.owner.filter("emit",e=>{switch(e.$lastStartTime=Date.now(),e.mode){case 0:{const i=t+e.$ref.clientWidth;e.$ref.style.left=`${t}px`,e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition=`transform ${e.$restTime}s linear 0s`;break}}})}continue(){const{clientWidth:t}=this.owner.$player;return this.owner.filter("stop",e=>{switch(this.owner.setState(e,"emit"),e.$lastStartTime=Date.now(),e.mode){case 0:{const i=t+e.$ref.clientWidth;e.$ref.style.transform=`translateX(${-i}px)`,e.$ref.style.transition=`transform ${e.$restTime}s linear 0s`;break}}}),this}suspend(){const{clientWidth:t}=this.owner.$player;return this.owner.filter("emit",e=>{switch(this.owner.setState(e,"stop"),e.mode){case 0:{const i=t-(this.owner.getLeft(e.$ref)-this.owner.getLeft(this.owner.$player));e.$ref.style.transform=`translLine truncated
|
||||
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDanmuku=e(),define(function(){return t.artplayerPluginDanmuku})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDanmuku=e()}(this,function(){"use strict";var t=Object.defineProperty,e=Object.defineProperties,i=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,r=Math.pow,o=(e,i,n)=>i in e?t(e,i,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[i]=n,l=(t,e)=>{for(var i in e||(e={}))a.call(e,i)&&o(t,i,e[i]);if(n)for(var i of n(e))s.call(e,i)&&o(t,i,e[i]);return t},d=(t,n)=>e(t,i(n)),c=(t,e,i)=>new Promise((n,a)=>{var s=t=>{try{o(i.next(t))}catch(e){a(e)}},r=t=>{try{o(i.throw(t))}catch(e){a(e)}},o=t=>t.done?n(t.value):Promise.resolve(t.value).then(s,r);o((i=i.apply(t,e)).next())});function p(t){return t instanceof Promise||"[object Promise]"===Object.prototype.toString.call(t)}function h(t){switch(t){case 1:case 2:case 3:default:return 0;case 4:return 2;case 5:return 1}}function u(t){if("string"!==typeof t)return[];const e=new RegExp('<d[^>]*?p="(?<p>[^"]+)"[^>]*>(?<text>.*?)<\\/d>',"gs"),i=t.matchAll(e);return Array.from(i).map(t=>{const e=t.groups.p.split(",");if(e.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(e[0]),mode:h(Number(e[1])),fontSize:Number(e[2]),color:`#${Number(e[3]).toString(16)}`,timestamp:Number(e[4]),pool:Number(e[5]),userID:e[6],rowID:Number(e[7])}}return null}).filter(Boolean)}function m({data:t}){const{xml:e,id:i}=t,n=u(e);globalThis.postMessage({danmus:n,id:i})}let f=0;function g(t,{signal:e,onCancel:i}={}){return new Promise((n,a)=>{let s,r,o,l=!1;const d=++f,p=()=>y(void 0,function(t){if(void 0!==(null==t?void 0:t.reason))return t.reason;const e=new Error("Bilibili Danmu loading cancelled");return e.name="AbortError",e}(e));function g(){let t,e=!1;try{if(s){const t=s;s=void 0,t.onmessage=null,t.onerror=null,t.onmessageerror=null,t.terminate()}}catch(i){t=i,e=!0}if(void 0!==r){const n=r;r=void 0;try{URL.revokeObjectURL(n)}catch(i){e||(t=i),e=!0}}if(e)throw t}function y(t,i){if(l)return;l=!0;let s=arguments.length>1;null==e||e.removeEventListener("abort",p),null==o||o();try{g()}catch(r){s||(i=r),s=!0}s?a(i):n(t)}function v(t,e){if(!l){console.error("Error parsing Bilibili Danmu:",e);try{y(u(t))}catch(i){y(void 0,i)}}}(null==e?void 0:e.aborted)?p():(null==e||e.addEventListener("abort",p),o=null==i?void 0:i(p),l?null==o||o():function(){return c(this,null,function*(){const i=e?yield fetch(t,{signal:e}):yield fetch(t);if(l)return;const n=yield i.text();if(!l)try{const t=`\n ${h.toString()}\n ${u.toString()}\n onmessage = ${m.toString()}\n `;if(r=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),l)return void g();if(s=new Worker(r),l)return void g();s.onmessage=t=>{var e;if(l||(null==(e=t.data)?void 0:e.id)!==d)return;const{danmus:i}=t.data;Array.isArray(i)?y(i):y(void 0,new Error("Invalid Bilibili Danmu worker response"))},s.onerror=t=>{var e;null==(e=t.preventDefault)||e.call(t),v(n,t.error||t)},s.onmessageerror=t=>v(n,t.error||t),s.postMessage({xml:n,id:d})}catch(a){v(n,a)}})}().catch(t=>y(void 0,t)))})}const y=new WeakMap,v=Symbol("cancelled danmuku input");function w(t){return y.has(t)||y.set(t,{closed:!1,tasks:new Set,emitting:[]}),y.get(t)}function k(t){const e=w(t),i=e.emitting[e.emitting.length-1];return!e.closed&&!t.art.isDestroy&&(!i||i.active())}function x(t){const e=w(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}function b(t,e,i){const n=t.states[e]||[];for(let a=0;a<n.length;a++)i(n[a]);return n}class ${constructor(t){this.owner=t,this.nodes=new Set}acquire(){const t=this.owner.$refs.pop()||document.createElement("div");return t.style.cssText=this.owner.constructor.cssText,t.dataset.mode="",t.dataset.id="",t.className="",this.nodes.add(t),t}get visibles(){const t=[],{clientWidth:e}=this.owner.$player,i=this.owner.getLeft(this.owner.$player);return this.owner.filter("emit",n=>{const a=n.$ref.offsetTop,s=this.owner.getLeft(n.$ref)-i,r=n.$ref.clientHeight,o=n.$ref.clientWidth,l=s+o,d=e-l,c=l/n.$restTime,p={};p.top=a,p.left=s,p.height=r,p.width=o,p.right=d,p.speed=c,p.distance=l,p.time=n.$restTime,p.mode=n.mode,t.push(p)}),t}left(t){const e=t.getBoundingClientRect();return this.owner.isRotate?e.top:e.left}makeWait(t){this.owner.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=this.owner.constructor.cssText,t.$ref.style.visibility="hidden",t.$ref.style.marginLeft="0px",t.$ref.style.transform="translateX(0px)",t.$ref.style.transition="transform 0s linear 0s",this.owner.$refs.push(t.$ref),t.$ref=null)}resize(){const{clientWidth:t}=this.owner.$player;this.owner.filter("stop",e=>{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.owner.filter("emitLine truncated
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDanmuku=e(),define(function(){return t.artplayerPluginDanmuku})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDanmuku=e()}(this,function(){"use strict";var t=Object.defineProperty,e=Object.defineProperties,i=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,r=Math.pow,o=(e,i,n)=>i in e?t(e,i,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[i]=n,l=(t,e)=>{for(var i in e||(e={}))a.call(e,i)&&o(t,i,e[i]);if(n)for(var i of n(e))s.call(e,i)&&o(t,i,e[i]);return t},d=(t,n)=>e(t,i(n)),c=(t,e,i)=>new Promise((n,a)=>{var s=t=>{try{o(i.next(t))}catch(e){a(e)}},r=t=>{try{o(i.throw(t))}catch(e){a(e)}},o=t=>t.done?n(t.value):Promise.resolve(t.value).then(s,r);o((i=i.apply(t,e)).next())});function p(t){return t instanceof Promise||"[object Promise]"===Object.prototype.toString.call(t)}function h(t){switch(t){case 1:case 2:case 3:default:return 0;case 4:return 2;case 5:return 1}}function u(t){if("string"!==typeof t)return[];const e=new RegExp('<d[^>]*?p="(?<p>[^"]+)"[^>]*>(?<text>.*?)<\\/d>',"gs"),i=t.matchAll(e);return Array.from(i).map(t=>{const e=t.groups.p.split(",");if(e.length>=8){return{text:t.groups.text.trim().replaceAll(""",'"').replaceAll("'","'").replaceAll("<","<").replaceAll(">",">").replaceAll("&","&"),time:Number(e[0]),mode:h(Number(e[1])),fontSize:Number(e[2]),color:`#${Number(e[3]).toString(16)}`,timestamp:Number(e[4]),pool:Number(e[5]),userID:e[6],rowID:Number(e[7])}}return null}).filter(Boolean)}function m({data:t}){const{xml:e,id:i}=t,n=u(e);globalThis.postMessage({danmus:n,id:i})}let f=0;function g(t,{signal:e,onCancel:i}={}){return new Promise((n,a)=>{let s,r,o,l=!1;const d=++f,p=()=>y(void 0,function(t){if(void 0!==(null==t?void 0:t.reason))return t.reason;const e=new Error("Bilibili Danmu loading cancelled");return e.name="AbortError",e}(e));function g(){let t,e=!1;try{if(s){const t=s;s=void 0,t.onmessage=null,t.onerror=null,t.onmessageerror=null,t.terminate()}}catch(i){t=i,e=!0}if(void 0!==r){const n=r;r=void 0;try{URL.revokeObjectURL(n)}catch(i){e||(t=i),e=!0}}if(e)throw t}function y(t,i){if(l)return;l=!0;let s=arguments.length>1;null==e||e.removeEventListener("abort",p),null==o||o();try{g()}catch(r){s||(i=r),s=!0}s?a(i):n(t)}function v(t,e){if(!l){console.error("Error parsing Bilibili Danmu:",e);try{y(u(t))}catch(i){y(void 0,i)}}}(null==e?void 0:e.aborted)?p():(null==e||e.addEventListener("abort",p),o=null==i?void 0:i(p),l?null==o||o():function(){return c(this,null,function*(){const i=e?yield fetch(t,{signal:e}):yield fetch(t);if(l)return;const n=yield i.text();if(!l)try{const t=`\n ${h.toString()}\n ${u.toString()}\n onmessage = ${m.toString()}\n `;if(r=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),l)return void g();if(s=new Worker(r),l)return void g();s.onmessage=t=>{var e;if(l||(null==(e=t.data)?void 0:e.id)!==d)return;const{danmus:i}=t.data;Array.isArray(i)?y(i):y(void 0,new Error("Invalid Bilibili Danmu worker response"))},s.onerror=t=>{var e;null==(e=t.preventDefault)||e.call(t),v(n,t.error||t)},s.onmessageerror=t=>v(n,t.error||t),s.postMessage({xml:n,id:d})}catch(a){v(n,a)}})}().catch(t=>y(void 0,t)))})}const y=new WeakMap,v=Symbol("cancelled danmuku input");function w(t){return y.has(t)||y.set(t,{closed:!1,tasks:new Set,emitting:[]}),y.get(t)}function k(t){const e=w(t),i=e.emitting[e.emitting.length-1];return!e.closed&&!t.art.isDestroy&&(!i||i.active())}function x(t){const e=w(t);e.closed=!0;for(const i of[...e.tasks])i.cancel()}function b(t,e,i){const n=t.states[e]||[];for(let a=0;a<n.length;a++)i(n[a]);return n}class ${constructor(t){this.owner=t,this.nodes=new Set}acquire(){const t=this.owner.$refs.pop()||document.createElement("div");return t.style.cssText=this.owner.constructor.cssText,t.dataset.mode="",t.dataset.id="",t.className="",this.nodes.add(t),t}get visibles(){const t=[],{clientWidth:e}=this.owner.$player,i=this.owner.getLeft(this.owner.$player);return this.owner.filter("emit",n=>{const a=n.$ref.offsetTop,s=this.owner.getLeft(n.$ref)-i,r=n.$ref.clientHeight,o=n.$ref.clientWidth,l=s+o,d=e-l,c=l/n.$restTime,p={};p.top=a,p.left=s,p.height=r,p.width=o,p.right=d,p.speed=c,p.distance=l,p.time=n.$restTime,p.mode=n.mode,t.push(p)}),t}left(t){const e=t.getBoundingClientRect();return this.owner.isRotate?e.top:e.left}makeWait(t){this.owner.setState(t,"wait"),t.$ref&&(t.$ref.style.cssText=this.owner.constructor.cssText,t.$ref.style.visibility="hidden",t.$ref.style.marginLeft="0px",t.$ref.style.transform="translateX(0px)",t.$ref.style.transition="transform 0s linear 0s",this.owner.$refs.push(t.$ref),t.$ref=null)}resize(){const{clientWidth:t}=this.owner.$player;this.owner.filter("stop",e=>{if(0===e.mode)e.$ref.style.left=`${t}px`}),this.owner.filter("emitLine truncated
|
||||
@@ -665,16 +665,18 @@ 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,
|
||||
// Valid margins are numeric. Preserve the old raw fallback for unsupported strings.
|
||||
marginBottom: owner.marginBottom,
|
||||
marginTop: owner.marginTop
|
||||
}));
|
||||
const top = typeof reply === "symbol" ? void 0 : reply.result;
|
||||
if (!this.active(operation) || danmu.$ref !== ref)
|
||||
return;
|
||||
if (top !== void 0) {
|
||||
@@ -948,6 +950,7 @@ class Danmuku {
|
||||
if (!task.active())
|
||||
return this;
|
||||
errorHandle(Array.isArray(danmus), "Danmuku need return an array as result");
|
||||
const rows = danmus;
|
||||
if (danmuku === void 0) {
|
||||
this.reset();
|
||||
if (!task.active())
|
||||
@@ -956,10 +959,10 @@ class Danmuku {
|
||||
this.states = { wait: [], ready: [], emit: [], stop: [] };
|
||||
this.renderer.clear();
|
||||
}
|
||||
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);
|
||||
}
|
||||
if (task.active())
|
||||
@@ -1074,6 +1077,7 @@ class Danmuku {
|
||||
return this.renderer.left($ref);
|
||||
}
|
||||
// 复杂运算交给 Web Worker 处理
|
||||
// Keep the historical empty default; unknown message types receive no reply.
|
||||
postMessage(message = {}) {
|
||||
return this.workerClient.request(message);
|
||||
}
|
||||
@@ -1312,8 +1316,9 @@ function heatmap(art, danmuku, option) {
|
||||
let closed = false;
|
||||
const active = () => !closed && !art.isDestroy;
|
||||
function listen(name, callback) {
|
||||
subscriptions.push([name, callback]);
|
||||
art.on(name, callback);
|
||||
const listener = callback;
|
||||
subscriptions.push([name, listener]);
|
||||
art.on(name, listener);
|
||||
}
|
||||
function dispose(removeControl) {
|
||||
if (closed)
|
||||
@@ -1351,18 +1356,19 @@ function heatmap(art, danmuku, option) {
|
||||
return;
|
||||
start = null;
|
||||
stop = null;
|
||||
element.innerHTML = "";
|
||||
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 document2 = element.ownerDocument;
|
||||
const document2 = target.ownerDocument;
|
||||
if (!gradient)
|
||||
gradient = document2 ? "heatmap-solids" : `heatmap-solids-${++nextGradient}`;
|
||||
while (document2?.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%">
|
||||
@@ -1375,8 +1381,8 @@ 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) {
|
||||
@@ -1456,7 +1462,8 @@ class SettingLifecycle {
|
||||
dispose();
|
||||
else this.disposers.add(dispose);
|
||||
}
|
||||
write(target, key, value, apply = (value2) => target[key] = value2) {
|
||||
write(host, key, value, apply = (value2) => host[key] = value2) {
|
||||
const target = host;
|
||||
if (!this.active)
|
||||
return;
|
||||
const priority = () => typeof target.getPropertyPriority === "function" ? target.getPropertyPriority(key) : void 0;
|
||||
@@ -1847,16 +1854,17 @@ const style = ".artplayer-plugin-danmuku {\n display: flex;\n position: relati
|
||||
if (typeof document !== "undefined") {
|
||||
const id = "artplayer-plugin-danmuku";
|
||||
const pending = /* @__PURE__ */ Symbol.for("artplayer-plugin-danmuku.pending-style");
|
||||
let $style2 = document.getElementById(id) || document[pending];
|
||||
const styleDocument = document;
|
||||
let $style2 = document.getElementById(id) || styleDocument[pending];
|
||||
if (!$style2) {
|
||||
$style2 = document.createElement("style");
|
||||
$style2.id = id;
|
||||
if (document.readyState === "loading") {
|
||||
document[pending] = $style2;
|
||||
styleDocument[pending] = $style2;
|
||||
const ready = () => {
|
||||
document.removeEventListener("DOMContentLoaded", ready);
|
||||
if (document[pending] === $style2)
|
||||
delete document[pending];
|
||||
if (styleDocument[pending] === $style2)
|
||||
delete styleDocument[pending];
|
||||
const existing = document.getElementById(id);
|
||||
if (existing)
|
||||
existing.textContent = $style2.textContent;
|
||||
|
||||
@@ -27,11 +27,31 @@
|
||||
"types": "./types/artplayer-plugin-danmuku.d.ts",
|
||||
"import": "./dist/artplayer-plugin-danmuku.legacy.js",
|
||||
"require": "./dist/artplayer-plugin-danmuku.legacy.js"
|
||||
},
|
||||
"./runtime": {
|
||||
"import": {
|
||||
"types": "./types/runtime.d.mts",
|
||||
"default": "./dist/artplayer-plugin-danmuku.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./types/runtime.d.cts",
|
||||
"default": "./dist/artplayer-plugin-danmuku.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main": "./dist/artplayer-plugin-danmuku.js",
|
||||
"module": "./dist/artplayer-plugin-danmuku.mjs",
|
||||
"types": "./types/artplayer-plugin-danmuku.d.ts",
|
||||
"legacy": "./dist/artplayer-plugin-danmuku.legacy.js",
|
||||
"browserslist": "last 1 Chrome version"
|
||||
"browserslist": "last 1 Chrome version",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"legacy": [
|
||||
"types/artplayer-plugin-danmuku.d.ts"
|
||||
],
|
||||
"runtime": [
|
||||
"types/runtime.d.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+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)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": false,
|
||||
"lib": ["ES2020", "ES2021.String", "DOM", "DOM.Iterable"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "../../types/assets.d.ts", "../../types/option-validator.d.ts"]
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
|
||||
export type Mode = 0 | 1 | 2
|
||||
export type State = 'wait' | 'ready' | 'emit' | 'stop'
|
||||
export type Margin = [number | string, number | string]
|
||||
/** Mutable tuples: historical custom heatmap rendering adjusts the second coordinate. */
|
||||
export type Point = [number, number]
|
||||
|
||||
export interface Danmu {
|
||||
text: string
|
||||
id?: string
|
||||
mode?: number
|
||||
color?: string
|
||||
time?: number
|
||||
border?: boolean
|
||||
style?: Partial<CSSStyleDeclaration>
|
||||
[extension: string]: unknown
|
||||
}
|
||||
|
||||
/** Input fields have been filled before filter runs; queue fields do not exist yet. */
|
||||
export interface NormalizedDanmu extends Danmu {
|
||||
mode: number
|
||||
color: string
|
||||
time: number
|
||||
style: Partial<CSSStyleDeclaration>
|
||||
}
|
||||
|
||||
/** The actual mutable queue entry supplied to beforeVisible and visible/loaded events. */
|
||||
export interface Item extends NormalizedDanmu {
|
||||
$state: State
|
||||
$index: number
|
||||
$ref: HTMLDivElement | null
|
||||
$restTime: number
|
||||
$lastStartTime: number
|
||||
}
|
||||
|
||||
/** Functions run without an option receiver. Arrays are also valid synchronous results. */
|
||||
export type Input = Danmu[] | string | Promise<Danmu[]> | (() => Danmu[] | Promise<Danmu[]>)
|
||||
export interface SliderStep<Value = number> { name?: string, value?: Value, hide?: boolean, show?: boolean }
|
||||
export interface Slider<Value = number> { min?: number, max?: number, steps?: SliderStep<Value>[] }
|
||||
export interface Heatmap {
|
||||
xMin?: number
|
||||
xMax?: number
|
||||
yMin?: number
|
||||
yMax?: number
|
||||
scale?: number
|
||||
opacity?: number
|
||||
minHeight?: number
|
||||
sampling?: number
|
||||
smoothing?: number
|
||||
flattening?: number
|
||||
}
|
||||
|
||||
export interface NormalizedOption {
|
||||
danmuku: Input
|
||||
speed: number
|
||||
margin: Margin
|
||||
opacity: number
|
||||
color: string
|
||||
/** Numeric inputs are clamped; values other than 0, 1 and 2 are not emitted. */
|
||||
mode: number
|
||||
modes: number[]
|
||||
fontSize: number | string
|
||||
antiOverlap: boolean
|
||||
synchronousPlayback: boolean
|
||||
mount: HTMLDivElement | string | undefined
|
||||
heatmap: boolean | Heatmap
|
||||
width: number
|
||||
/** Stored configuration; send the points event to draw custom points. */
|
||||
points: Point[]
|
||||
/** A truthy result accepts the row. */
|
||||
filter: (this: NormalizedOption, danmu: NormalizedDanmu) => unknown
|
||||
/** Only a result equal to true sends the row; callbacks may be asynchronous. */
|
||||
beforeEmit: (this: NormalizedOption, danmu: Danmu) => unknown
|
||||
/** A truthy result displays the queue entry; callbacks may be asynchronous. */
|
||||
beforeVisible: (this: NormalizedOption, danmu: Item) => unknown
|
||||
visible: boolean
|
||||
emitter: boolean
|
||||
maxLength: number
|
||||
lockTime: number
|
||||
theme: string
|
||||
OPACITY: Slider
|
||||
FONT_SIZE: Slider
|
||||
MARGIN: Slider<Margin>
|
||||
SPEED: Slider
|
||||
COLOR: string[]
|
||||
}
|
||||
|
||||
/** The argument object is required; every option inside it is optional. */
|
||||
export type RuntimeOption = Partial<NormalizedOption>
|
||||
|
||||
/** The owner returned by commands is different from the registered plugin facade. */
|
||||
export interface Owner {
|
||||
art: Artplayer
|
||||
option: NormalizedOption
|
||||
isHide: boolean
|
||||
isStop: boolean
|
||||
queue: Item[]
|
||||
states: Record<State, Item[]>
|
||||
readonly readys: Item[]
|
||||
readonly speed: number
|
||||
readonly fontSize: number
|
||||
readonly marginTop: number
|
||||
readonly marginBottom: number | string
|
||||
readonly isRotate: boolean | undefined
|
||||
emit: (danmu: Danmu) => Promise<Owner>
|
||||
/** No argument replaces the configured source; a provided argument appends rows. */
|
||||
load: (danmuku?: Input) => Promise<Owner>
|
||||
config: (option: RuntimeOption) => Owner
|
||||
hide: () => Owner
|
||||
show: () => Owner
|
||||
reset: () => Owner
|
||||
start: () => Owner
|
||||
stop: () => Owner
|
||||
continue: () => Owner
|
||||
suspend: () => Owner
|
||||
update: () => Owner
|
||||
resize: () => void
|
||||
seek: () => void
|
||||
destroy: () => void
|
||||
}
|
||||
|
||||
export interface RuntimeResult {
|
||||
name: 'artplayerPluginDanmuku'
|
||||
emit: (danmu: Danmu) => Promise<Owner>
|
||||
load: (danmuku?: Input) => Promise<Owner>
|
||||
config: (option: RuntimeOption) => Owner
|
||||
hide: () => Owner
|
||||
show: () => Owner
|
||||
reset: () => Owner
|
||||
/** The live setting requires a valid mount; omitting it does not select a default. */
|
||||
mount: (target: HTMLDivElement | string) => void
|
||||
readonly option: NormalizedOption
|
||||
readonly isHide: boolean
|
||||
readonly isStop: boolean
|
||||
}
|
||||
|
||||
export interface Icons {
|
||||
$on: string
|
||||
$off: string
|
||||
$config: string
|
||||
$style: string
|
||||
$mode_0_off: string
|
||||
$mode_0_on: string
|
||||
$mode_1_off: string
|
||||
$mode_1_on: string
|
||||
$mode_2_off: string
|
||||
$mode_2_on: string
|
||||
$check_on: string
|
||||
$check_off: string
|
||||
}
|
||||
|
||||
export interface RuntimeFactory {
|
||||
(option: RuntimeOption): (art: Artplayer) => RuntimeResult
|
||||
icons: Icons
|
||||
}
|
||||
|
||||
/** Explicit payload map; importing /runtime does not augment historical Artplayer events. */
|
||||
export interface EventMap {
|
||||
'artplayerPluginDanmuku:points': [points: Point[]]
|
||||
'artplayerPluginDanmuku:loaded': [queue: Item[]]
|
||||
'artplayerPluginDanmuku:visible': [item: Item]
|
||||
'artplayerPluginDanmuku:config': [option: NormalizedOption]
|
||||
'artplayerPluginDanmuku:error': [error: unknown]
|
||||
'artplayerPluginDanmuku:show': []
|
||||
'artplayerPluginDanmuku:hide': []
|
||||
'artplayerPluginDanmuku:start': []
|
||||
'artplayerPluginDanmuku:stop': []
|
||||
'artplayerPluginDanmuku:reset': []
|
||||
'artplayerPluginDanmuku:destroy': []
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { RuntimeFactory } from './runtime-shared.js'
|
||||
|
||||
declare const artplayerPluginDanmuku: RuntimeFactory
|
||||
// eslint-disable-next-line ts/no-redeclare -- Ambient merge exposes types on the CommonJS factory.
|
||||
declare namespace artplayerPluginDanmuku {
|
||||
type Danmu = import('./runtime-shared.js').Danmu
|
||||
type EventMap = import('./runtime-shared.js').EventMap
|
||||
type Heatmap = import('./runtime-shared.js').Heatmap
|
||||
type Icons = import('./runtime-shared.js').Icons
|
||||
type Input = import('./runtime-shared.js').Input
|
||||
type Item = import('./runtime-shared.js').Item
|
||||
type Margin = import('./runtime-shared.js').Margin
|
||||
type Mode = import('./runtime-shared.js').Mode
|
||||
type NormalizedDanmu = import('./runtime-shared.js').NormalizedDanmu
|
||||
type NormalizedOption = import('./runtime-shared.js').NormalizedOption
|
||||
type Owner = import('./runtime-shared.js').Owner
|
||||
type Point = import('./runtime-shared.js').Point
|
||||
type RuntimeFactory = import('./runtime-shared.js').RuntimeFactory
|
||||
type RuntimeOption = import('./runtime-shared.js').RuntimeOption
|
||||
type RuntimeResult = import('./runtime-shared.js').RuntimeResult
|
||||
type Slider<Value = number> = import('./runtime-shared.js').Slider<Value>
|
||||
type SliderStep<Value = number> = import('./runtime-shared.js').SliderStep<Value>
|
||||
type State = import('./runtime-shared.js').State
|
||||
}
|
||||
export = artplayerPluginDanmuku
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { RuntimeFactory } from './runtime-shared.js'
|
||||
|
||||
declare const artplayerPluginDanmuku: RuntimeFactory
|
||||
export default artplayerPluginDanmuku
|
||||
export type { Danmu, EventMap, Heatmap, Icons, Input, Item, Margin, Mode, NormalizedDanmu, NormalizedOption, Owner, Point, RuntimeFactory, RuntimeOption, RuntimeResult, Slider, SliderStep, State } from './runtime-shared.js'
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { RuntimeFactory } from './runtime-shared.js'
|
||||
|
||||
declare const artplayerPluginDanmuku: RuntimeFactory
|
||||
// eslint-disable-next-line ts/no-redeclare -- Ambient merge exposes types on the CommonJS factory.
|
||||
declare namespace artplayerPluginDanmuku {
|
||||
type Danmu = import('./runtime-shared.js').Danmu
|
||||
type EventMap = import('./runtime-shared.js').EventMap
|
||||
type Heatmap = import('./runtime-shared.js').Heatmap
|
||||
type Icons = import('./runtime-shared.js').Icons
|
||||
type Input = import('./runtime-shared.js').Input
|
||||
type Item = import('./runtime-shared.js').Item
|
||||
type Margin = import('./runtime-shared.js').Margin
|
||||
type Mode = import('./runtime-shared.js').Mode
|
||||
type NormalizedDanmu = import('./runtime-shared.js').NormalizedDanmu
|
||||
type NormalizedOption = import('./runtime-shared.js').NormalizedOption
|
||||
type Owner = import('./runtime-shared.js').Owner
|
||||
type Point = import('./runtime-shared.js').Point
|
||||
type RuntimeFactory = import('./runtime-shared.js').RuntimeFactory
|
||||
type RuntimeOption = import('./runtime-shared.js').RuntimeOption
|
||||
type RuntimeResult = import('./runtime-shared.js').RuntimeResult
|
||||
type Slider<Value = number> = import('./runtime-shared.js').Slider<Value>
|
||||
type SliderStep<Value = number> = import('./runtime-shared.js').SliderStep<Value>
|
||||
type State = import('./runtime-shared.js').State
|
||||
}
|
||||
export = artplayerPluginDanmuku
|
||||
Reference in new issue
Block a user