UNPKG

vue3-danmaku

Version:

基于 Vue3 的弹幕交互组件

2 lines (1 loc) 12.1 kB
import{defineComponent as e,ref as n,reactive as t,computed as a,onMounted as o,onBeforeUnmount as s,nextTick as l,createApp as r,h as i,createElementBlock as u,openBlock as d,createElementVNode as c,renderSlot as m,normalizeClass as f}from"vue";const v=new Map,h=new Map;function p(e,n,t,a,o,s){e.style.transform="translateX(0px)",e.style.left=`${t}px`;const l=performance.now(),r=t/a*1e3,i=t,u=-n;h.set(e,i);const d=requestAnimationFrame((function n(t){if(o())return;const a=t-l;if(a>=r)return void s(e);const d=i+(u-i)*(a/r);e.style.transform=`translateX(${d-i}px)`,h.set(e,d);const c=requestAnimationFrame(n);v.set(e,c)}));v.set(e,d)}function g(e,n,t,a,o,s){const l=h.get(e);if(void 0===l)return void p(e,n,t,a,o,s);const r=t,i=-n,u=t/a*1e3,d=u*((r-l)/(r-i)),c=performance.now()-d;const m=requestAnimationFrame((function n(t){if(o())return;const a=t-c;if(a>=u)return void s(e);const l=r+(i-r)*(a/u);e.style.transform=`translateX(${l-r}px)`,h.set(e,l);const d=requestAnimationFrame(n);v.set(e,d)}));v.set(e,m)}function y(e){const n=v.get(e);n&&(cancelAnimationFrame(n),v.delete(e))}function w(){v.forEach((e=>{cancelAnimationFrame(e)})),v.clear(),h.clear()}var x=e({name:"vue-danmaku",components:{},props:{danmus:{type:Array,required:!0,default:()=>[]},channels:{type:Number,default:0},autoplay:{type:Boolean,default:!0},loop:{type:Boolean,default:!1},loopOnly:{type:Boolean,default:!1},randomChannel:{type:Boolean,default:!1},isSuspend:{type:Boolean,default:!1},performanceMode:{type:Boolean,default:!0},debounce:{type:Number,default:100},speeds:{type:Number,default:200},top:{type:Number,default:4},right:{type:Number,default:0},zIndex:{type:Number,default:1},autoResize:{type:Boolean,default:!0}},emits:["list-end","play-end","dm-over","dm-out","dm-click","dm-remove","error"],setup(e,{emit:u,slots:d,expose:c}){let m=n(document.createElement("div")),f=n(document.createElement("div"));const h=n(0),x=n(0);let k=0;const T=n(0),_=n(0),E=n(0),M=n(!1),A=n(!0),C=n({}),N=n([...e.danmus]),b=n(new Set);let L=null;!function(e,n,t="modelValue"){a({get:()=>e[t],set:e=>{n(`update:${t}`,e)}})}(e,u,"danmus");const F=t({channels:a((()=>e.channels||T.value)),debounce:a((()=>e.debounce)),randomChannel:a((()=>e.randomChannel))}),I=t({height:a((()=>_.value)),speeds:a((()=>e.speeds)),top:a((()=>e.top)),right:a((()=>e.right))});function $(){if(h.value=m.value.offsetWidth,x.value=m.value.offsetHeight,0===h.value||0===x.value){const e="获取不到容器宽高";u("error",{message:e,code:"CONTAINER_SIZE_ERROR"}),console.error(`[vue-danmaku] ${e}`)}}function D(){A.value&&(A.value=!1,k||(k=window.setInterval((()=>function(){if(!A.value&&N.value.length)if(E.value>N.value.length-1){const n=f.value.children.length;e.loop&&(n<E.value&&(u("list-end"),E.value=0,e.loopOnly&&b.value.clear()),S())}else S()}()),F.debounce)),e.performanceMode&&function(){if(f.value){Array.from(f.value.querySelectorAll(".dm")).forEach((e=>{g(e,e.offsetWidth,h.value,I.speeds,(()=>A.value),R)}))}}())}function S(n){const t=e.loop?E.value%N.value.length:E.value;if(e.loopOnly&&b.value.has(t))return;const a=function(e,n){const t=r({render:()=>i("div",{},[d.danmu&&d.danmu({danmu:e,index:n})])}),a=t.mount(document.createElement("div"));return a.$el.__vueApp=t,a}(n||N.value[t],t).$el;a.classList.add("dm"),f.value.appendChild(a),a.style.opacity="0",a._vueInstance={instance:a.__vueParentComponent||{ctx:{}},el:a},l((()=>{I.height||(_.value=a.offsetHeight),F.channels||(T.value=Math.floor(x.value/(I.height+I.top))),function(n,t){let a=function(e){let n=[...Array(F.channels).keys()];F.randomChannel&&(n=n.sort((()=>.5-Math.random())));for(let t of n){const n=C.value[t];if(!n||!n.length)return C.value[t]=[e],t%F.channels;for(let a=0;a<n.length;a++){const o=z(n[a])-10;if(o<=.88*(e.offsetWidth-n[a].offsetWidth)||o<=0)break;if(a===n.length-1)return C.value[t].push(e),t%F.channels}}return-1}(n);if(a>=0){const o=n.offsetWidth,s=I.height;n.dataset.index=`${t}`,n.dataset.channel=a.toString(),n.style.opacity="1",n.style.top=a*(s+I.top)+"px",n.style.width=o+I.right+"px",n.style.zIndex=e.zIndex.toString();const l=e=>{u("dm-click",{el:n,index:t,danmu:N.value[t],event:e})};if(n.addEventListener("click",l),n._clickHandler=l,e.loop&&e.loopOnly&&b.value.add(t),e.performanceMode)p(n,o,h.value,I.speeds,(()=>A.value),R);else{n.classList.add("move"),n.style.setProperty("--dm-scroll-width",`-${h.value+o}px`),n.style.left=`${h.value}px`,n.style.animationDuration=h.value/I.speeds+"s";const t=()=>{Number(n.dataset.index)!==N.value.length-1||e.loop||u("play-end",n.dataset.index);const t=Number(n.dataset.index);e.loop&&e.loopOnly&&t>=0&&b.value.delete(t),u("dm-remove",{el:n,index:t,danmu:t>=0?N.value[t]:null}),U(n),f.value&&n.parentNode===f.value&&f.value.removeChild(n)};n._animationEndHandler=t,n.addEventListener("animationend",t)}E.value++}else U(n),f.value&&n.parentNode===f.value&&f.value.removeChild(n)}(a,t)}))}function z(e){const n=e.offsetWidth||parseInt(e.style.width),t=e.getBoundingClientRect().right||f.value.getBoundingClientRect().right+n;return f.value.getBoundingClientRect().right-t}function B(){clearInterval(k),k=0,E.value=0,b.value.clear()}function O(){if(B(),f.value){Array.from(f.value.querySelectorAll(".dm")).forEach((e=>{U(e)})),f.value.innerHTML=""}e.performanceMode&&w(),C.value={},A.value=!0}function R(n){Number(n.dataset.index)!==N.value.length-1||e.loop||u("play-end",n.dataset.index);const t=Number(n.dataset.index);e.loop&&e.loopOnly&&t>=0&&b.value.delete(t),u("dm-remove",{el:n,index:E,danmu:t>=0?N.value[t]:null}),U(n),f.value&&n.parentNode===f.value&&f.value.removeChild(n)}function W(){const n=h.value;$();const t=f.value.getElementsByClassName("dm");for(let a=0;a<t.length;a++){const o=t[a],s=o.offsetWidth;if(e.performanceMode){let e=(n-(o.getBoundingClientRect().left-m.value.getBoundingClientRect().left))/(n+s);if(e=Math.max(0,Math.min(1,e)),y(o),e>=1)R(o);else{const n=h.value-(h.value+s)*e;o.style.left=`${h.value}px`,o.style.transform=`translateX(${n-h.value}px)`,g(o,s,h.value,I.speeds,(()=>A.value),R)}}else o.style.setProperty("--dm-scroll-width",`-${h.value+s}px`),o.style.left=`${h.value}px`,o.style.animationDuration=h.value/I.speeds+"s"}}o((()=>{!function(){$(),e.isSuspend&&(f.value.addEventListener("mouseover",P),f.value.addEventListener("mouseout",q)),d.danmu||(u("error",{message:'没有提供弹幕插槽内容(slot="danmu"),无法展示弹幕',code:"NO_DANMU_SLOT"}),console.error('[vue-danmaku] 警告:没有提供弹幕插槽内容(slot="danmu"),无法展示弹幕'));e.autoplay&&D()}(),e.autoResize&&function(){if("undefined"!=typeof ResizeObserver)L=new ResizeObserver((()=>{W()})),L.observe(m.value);else{const e=()=>W();window.addEventListener("resize",e),s((()=>{window.removeEventListener("resize",e)}))}}()})),s((()=>{O(),f.value&&(f.value.removeEventListener("mouseover",P),f.value.removeEventListener("mouseout",q)),L&&(L.disconnect(),L=null),e.performanceMode&&w()}));let H=[];function P(n){let t=n.target;t.className.includes("dm")||(t=t.closest(".dm")||t),t.className.includes("dm")&&(H.includes(t)||(u("dm-over",{el:t}),t.style.zIndex=(e.zIndex+1).toString(),e.performanceMode?y(t):t.classList.add("pause"),H.push(t)))}function q(e){let n=e.target;n.className.includes("dm")||(n=n.closest(".dm")||n),n.className.includes("dm")&&(u("dm-out",{el:n}),X(n),H.forEach(X),H=[])}function X(n){if(n.style.zIndex=e.zIndex.toString(),e.performanceMode){g(n,n.offsetWidth,h.value,I.speeds,(()=>A.value),R)}else n.classList.remove("pause")}function U(n){e.performanceMode?y(n):n._animationEndHandler&&(n.removeEventListener("animationend",n._animationEndHandler),delete n._animationEndHandler),n._clickHandler&&(n.removeEventListener("click",n._clickHandler),delete n._clickHandler);const t=n.dataset.channel?parseInt(n.dataset.channel):-1;if(t>=0&&C.value[t]){const e=C.value[t].indexOf(n);e>-1&&C.value[t].splice(e,1)}if(n.__vueApp){try{n.__vueApp.unmount()}catch(e){console.warn("卸载组件实例失败",e)}delete n.__vueApp}n._vueInstance&&delete n._vueInstance}return{container:m,dmContainer:f,hidden:M,paused:A,danmuList:N,getPlayState:function(){return!A.value},getMaxChannels:function(){return I.height?Math.floor(x.value/(I.height+I.top)):0},resize:W,play:D,pause:function(){A.value=!0,e.performanceMode&&(v.forEach((e=>{cancelAnimationFrame(e)})),v.clear())},stop:O,show:function(){M.value=!1},hide:function(){M.value=!0},clear:B,reset:function(){O(),_.value=0,M.value=!1,$()},addDanmu:function(e,n="current"){if("current"===n){if(E.value===N.value.length)return N.value.push(e),N.value.length-1;{const n=E.value%N.value.length;return N.value.splice(n,0,e),n+1}}return N.value.push(e),N.value.length-1},insert:S}}});const k={ref:"container",class:"vue-danmaku"};!function(e,n){void 0===n&&(n={});var t=n.insertAt;if("undefined"!=typeof document){var a=document.head||document.getElementsByTagName("head")[0],o=document.createElement("style");o.type="text/css","top"===t&&a.firstChild?a.insertBefore(o,a.firstChild):a.appendChild(o),o.styleSheet?o.styleSheet.cssText=e:o.appendChild(document.createTextNode(e))}}(".vue-danmaku {\n position: relative;\n overflow: hidden;\n height: 100%;\n width: 100%;\n}\n.vue-danmaku .danmus {\n position: absolute;\n left: 0;\n top: 0;\n width: 100%;\n height: 100%;\n opacity: 0;\n -webkit-transition: all 0.3s;\n transition: all 0.3s;\n}\n.vue-danmaku .danmus.show {\n opacity: 1;\n}\n.vue-danmaku .danmus.paused .dm.move {\n animation-play-state: paused;\n}\n.vue-danmaku .danmus .dm {\n position: absolute;\n font-size: 20px;\n color: #666;\n white-space: pre;\n cursor: default;\n transform: translateX(0);\n transform-style: preserve-3d;\n}\n.vue-danmaku .danmus .dm.move {\n will-change: transform;\n animation-name: moveLeft;\n animation-timing-function: linear;\n animation-play-state: running;\n}\n.vue-danmaku .danmus .dm.pause {\n animation-play-state: paused;\n}\n@keyframes moveLeft {\n from {\n transform: translateX(0);\n }\n to {\n transform: translateX(var(--dm-scroll-width));\n }\n}\n@-webkit-keyframes moveLeft {\n from {\n -webkit-transform: translateX(0);\n }\n to {\n -webkit-transform: translateX(var(--dm-scroll-width));\n }\n}"),x.render=function(e,n,t,a,o,s){return d(),u("div",k,[c("div",{ref:"dmContainer",class:f(["danmus",{show:!e.hidden},{paused:e.paused}])},null,2),m(e.$slots,"default")],512)},x.__file="src/lib/Danmaku.vue";class T{constructor(e=30,n){this.fpsMonitorTimer=0,this.lastTime=0,this.frames=0,this.fps=0,this.running=!1,this.fpsThreshold=e,this.onFpsDrop=n||(()=>{})}start(){if(this.running)return;this.running=!0;const e=()=>{if(!this.running)return;const n=performance.now();this.frames++,n>this.lastTime+1e3&&(this.fps=Math.round(1e3*this.frames/(n-this.lastTime)),this.fps<this.fpsThreshold&&this.onFpsDrop({fps:this.fps,threshold:this.fpsThreshold}),this.frames=0,this.lastTime=n),requestAnimationFrame(e)};this.fpsMonitorTimer=window.setTimeout((()=>{this.lastTime=performance.now(),requestAnimationFrame(e)}),1e3)}stop(){this.running=!1,this.fpsMonitorTimer&&(clearTimeout(this.fpsMonitorTimer),this.fpsMonitorTimer=0)}getCurrentFps(){return this.fps}setFpsDropCallback(e){this.onFpsDrop=e}setFpsThreshold(e){this.fpsThreshold=e}}class _{constructor(e=100,n){this.warningThreshold=e,this.onPerformanceWarning=n||(()=>{})}checkPerformance(e){e>this.warningThreshold&&this.onPerformanceWarning({count:e,threshold:this.warningThreshold,message:`当前弹幕数量(${e})超过阈值(${this.warningThreshold}),可能影响性能`})}setWarningCallback(e){this.onPerformanceWarning=e}setWarningThreshold(e){this.warningThreshold=e}}function E(e,n){return new T(e,n)}function M(e,n){return new _(e,n)}function A(e={}){const n=E(e.fpsThreshold,e.onFpsDrop),t=M(e.warningThreshold,e.onPerformanceWarning);return{fpsMonitor:n,performanceMonitor:t,startAll(){n.start()},stopAll(){n.stop()},checkDanmakuCount(e){t.checkPerformance(e)}}}const C={DEFAULT_FPS_THRESHOLD:30,DEFAULT_WARNING_DANMU_COUNT:100};export{x as Danmaku,C as PERFORMANCE_CONSTANTS,A as createDanmakuMonitor,M as createDanmakuPerformanceMonitor,E as createFpsMonitor,x as default};