<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          淺談彈幕的設(shè)計(jì)

          共 11960字,需瀏覽 24分鐘

           ·

          2021-08-18 06:53

           大廠技術(shù)  堅(jiān)持周更  精選好文


          背景

          為了創(chuàng)造更好的多媒體體驗(yàn),許多視頻網(wǎng)站都添加了社交機(jī)制,使用戶(hù)可以在媒體時(shí)間軸上的特定點(diǎn)發(fā)布評(píng)論和查看其他人的評(píng)論,其中一種機(jī)制被稱(chēng)為彈幕(dàn mù),在日語(yǔ)中也稱(chēng)為コメント(comment)或者弾幕(danmaku),在播放過(guò)程中,可能會(huì)出現(xiàn)大量評(píng)論和注釋?zhuān)⑶抑苯愉秩驹谝曨l上。彈幕最初是由日本視頻網(wǎng)站Niconico(ニコニコ)引入的。在中國(guó),除了在Bilibili和AcFun等彈幕視頻網(wǎng)站中使用之外,其他主流視頻網(wǎng)站(例如騰訊視頻,愛(ài)奇藝視頻,優(yōu)酷視頻和咪咕視頻)中的視頻播放器也支持彈幕。


          形式

          單條彈幕的基本模式有三種:

          1. 滾動(dòng)彈幕:自右向左滾動(dòng)過(guò)屏幕的彈幕,以自上而下的優(yōu)先度展示。
          2. 頂部彈幕:自上而下靜止居中的彈幕、以自上而下的優(yōu)先度展示。
          3. 底部彈幕:自下而上靜止居中的彈幕、以自下而上的優(yōu)先度展示。

          為什么需要彈幕

          從用戶(hù)體驗(yàn)角度出發(fā)——沒(méi)有彈幕之前

          在沒(méi)有彈幕之前,我們一般是通過(guò)評(píng)論或者聊天室的方式去進(jìn)行互動(dòng):(如上,左邊視頻,右邊互動(dòng)區(qū))

          傳統(tǒng)互動(dòng)方式帶來(lái)的問(wèn)題是,當(dāng)我們的人眼的關(guān)注點(diǎn)在視頻上時(shí),是沒(méi)辦法進(jìn)行“一眼二用”的,簡(jiǎn)單的來(lái)說(shuō)就是,你沒(méi)辦法讓你的兩顆眼珠子往不同的方向看。這樣帶來(lái)的弊端是,當(dāng)用戶(hù)專(zhuān)注于視頻時(shí),互動(dòng)區(qū)的交互效果是很差的;而當(dāng)用戶(hù)在看互動(dòng)區(qū)的評(píng)論時(shí),又沒(méi)辦法去關(guān)注整件事的主體內(nèi)容,顧此失彼。

          (你沒(méi)辦法“一眼二用”) 與此同時(shí),對(duì)于世界上大多數(shù)的人來(lái)說(shuō),自小養(yǎng)成的習(xí)慣就是從左往右的閱讀習(xí)慣。像這種互動(dòng)區(qū)的評(píng)論,通常都是從下往上進(jìn)行自動(dòng)滾動(dòng)的,兩個(gè)方向的合起來(lái)的話整個(gè)文字就形成了一個(gè)傾斜的運(yùn)動(dòng)方向,使得用戶(hù)的閱讀產(chǎn)生了障礙。(傾斜向上的文字移動(dòng),讓人沒(méi)辦法好好看字)

          從用戶(hù)體驗(yàn)角度出發(fā)——彈幕出現(xiàn)之后

          彈幕出現(xiàn)后,我們的視角就集中到視頻主體上,當(dāng)彈幕出現(xiàn)時(shí),如果是滾動(dòng)彈幕,那么一般都是從右往左出發(fā),非常適合我們的從左往右的閱讀習(xí)慣,并且,文字的移動(dòng)方向只有一個(gè),不會(huì)給我們的閱讀產(chǎn)生障礙。

          除此之外的好處

          互動(dòng)性強(qiáng):點(diǎn)播時(shí)讓你覺(jué)得不孤獨(dú)

          在觀看視頻網(wǎng)站提供視頻時(shí),觀看者在觀看視頻內(nèi)容過(guò)程中根據(jù)內(nèi)容啟發(fā)會(huì)有一些想法或者吐槽點(diǎn),就想要發(fā)表出來(lái)和更多的人分享,這時(shí)就需要彈幕來(lái)滿(mǎn)足這個(gè)需求。通過(guò)彈幕,可以把同一時(shí)間觀看者的評(píng)論通過(guò)固定方向滾動(dòng)的方式顯示在視頻區(qū)域中,或者靜止的顯示在視頻區(qū)域的頂部或底部,這樣可以增加觀看者和視頻的互動(dòng)特性以及觀看者之間的互動(dòng)。在相同時(shí)刻發(fā)送的彈幕基本上也具有相同的主題。

          互動(dòng)性強(qiáng):直播時(shí)的互動(dòng)及時(shí)

          彈幕在視頻直播場(chǎng)景中也能夠成為主播與觀眾直接互動(dòng)的方式。比起傳統(tǒng)的實(shí)時(shí)評(píng)論,主播能夠根據(jù)屏幕上彈幕的展現(xiàn)更直觀了解觀眾的需求和反饋,更方便地調(diào)整接下來(lái)的行動(dòng)和處理,也能夠根據(jù)用戶(hù)的輸入進(jìn)行交互操作。

          氣氛渲染好:“前方高能”

          當(dāng)看一些比較恐怖、懸疑的內(nèi)容時(shí),“前方高能”可能會(huì)避免你心里落下童年陰影[手動(dòng)狗頭]。

          彈幕的實(shí)現(xiàn)方式

          現(xiàn)如今,從B站、愛(ài)奇藝、騰訊視頻等各大媒體網(wǎng)站上按下 F12 時(shí),很容易發(fā)現(xiàn)是通過(guò) HTML+CSS 的方式實(shí)現(xiàn)的。另外,也有一小部分具備 Canvas 實(shí)現(xiàn)的彈幕,比如之前的B站(不過(guò)在截稿前好像找不到切換按鈕了)。

          假如通過(guò) HTML+CSS 實(shí)現(xiàn)

          通過(guò) DOM 元素實(shí)現(xiàn)彈幕,前端同學(xué)可以很方便地通過(guò) CSS 修改彈幕樣式。同時(shí),得益于瀏覽器原生的 DOM 事件機(jī)制,借助這個(gè)可以很快捷實(shí)現(xiàn)一系列彈幕交互功能:個(gè)性化、點(diǎn)贊、舉報(bào)等,以滿(mǎn)足產(chǎn)品的各種互動(dòng)需求。很容易看到,目前像騰訊視頻、愛(ài)奇藝等都是通過(guò) DOM 元素實(shí)現(xiàn)彈幕,這是目前主流的實(shí)現(xiàn)方式。

          假如通過(guò) Canvas 實(shí)現(xiàn)

          Canvas 為動(dòng)畫(huà)而生,但是基于 Canvas 實(shí)現(xiàn)一個(gè)彈幕系統(tǒng),會(huì)比基于 DOM 實(shí)現(xiàn)要復(fù)雜。暫且不說(shuō)對(duì)于大部分前端同學(xué)而言,對(duì) Canvas 的熟悉程度遠(yuǎn)比 DOM 要低,更何況,Canvas 并沒(méi)有一套原生的事件系統(tǒng),這意味著,如果要實(shí)現(xiàn)一些互動(dòng)功能,你必須要自己實(shí)現(xiàn)一套 Canvas 的事件機(jī)制……

          彈幕的設(shè)計(jì)

          首先是整體設(shè)計(jì),主要是三個(gè)部分:舞臺(tái)、軌道、彈幕池。

          舞臺(tái)

          舞臺(tái)是整個(gè)彈幕的主控制,它維護(hù)著多個(gè)軌道、一個(gè)等待隊(duì)列、一個(gè)彈幕池。舞臺(tái)要做的事情是控制整個(gè)彈幕的節(jié)奏,當(dāng)每一幀進(jìn)行渲染時(shí),都判斷其中的軌道是否有空位,從等待隊(duì)列中取合適的彈幕送往合適的軌道。舞臺(tái)的能力可以通過(guò)實(shí)現(xiàn)舞臺(tái)基類(lèi)以及對(duì)應(yīng)的抽象函數(shù),讓具體類(lèi)型的舞臺(tái)去實(shí)現(xiàn)對(duì)應(yīng)的舞臺(tái)邏輯。從而實(shí)現(xiàn)不同渲染能力(Canvas、HTML+CSS)以及不同類(lèi)型(滾動(dòng)、頂部固定、底部固定)的彈幕控制。無(wú)法復(fù)制加載中的內(nèi)容 不管是通過(guò) Canvas 還是 DOM 實(shí)現(xiàn)彈幕,需要的方法都是相似的:添加新彈幕到等待隊(duì)列、尋找合適的軌道、從等待隊(duì)列中抽取彈幕并放入軌道、整體渲染、清空。因此 BaseStage 可以通過(guò)編排抽象方法,讓具體的子類(lèi)去進(jìn)行具體實(shí)現(xiàn)。

          export default abstract class BaseStage<T extends BarrageObjectextends EventEmitter 
            protected trackWidth: number 
            protected trackHeight: number 
            protected duration: number 
            protected maxTrack: number 
            protected tracks: Track<T>[] = [] 
            waitingQueue: T[] = [] 
           
            // 添加彈幕到等待隊(duì)列 
            abstract add(barrage: T): boolean 
            // 尋找合適的軌道 
            abstract _findTrack(): number 
            // 從等待隊(duì)列中抽取彈幕并放入軌道 
            abstract _extractBarrage(): void 
            // 渲染函數(shù) 
            abstract render(): void 
            // 清空 
            abstract reset(): void 

          Canvas 版本

          比如,Canvas的舞臺(tái)基類(lèi)需要傳入Canvas元素,獲取Context。最后通過(guò)實(shí)現(xiàn) BaseStage 的抽象方法實(shí)現(xiàn)具體的邏輯。

          export default abstract class BaseCanvasStage<T extends BarrageObjectextends BaseStage
            T 

            protected canvas: HTMLCanvasElement 
            protected ctx: CanvasRenderingContext2D 
           
            constructor(canvas: HTMLCanvasElement, config: Config) { 
              super(config) 
              this.canvas = canvas 
              this.ctx = canvas.getContext('2d')! 
            } 

          HTML + CSS 版本

          而對(duì)于HTML+CSS的實(shí)現(xiàn),就需要維護(hù)一個(gè)彈幕池domPool、彈幕實(shí)例與DOM的映射關(guān)系(objToElm、elmToObj)以及一些必要的事件處理方法(_mouseMoveEventHandler 、_mouseClickEventHandler)。

          export default abstract class BaseCssStage<T extends BarrageObjectextends BaseStage<T
            el: HTMLDivElement 
            objToElmWeakMap<T, HTMLElement> = new WeakMap() 
            elmToObjWeakMap<HTMLElement, T> = new WeakMap() 
            freezeBarrage: T | null = null 
            domPoolArray<HTMLElement> = [] 
           
            constructor(el: HTMLDivElement, config: Config) { 
              super(config) 
           
              this.el = el 
           
              const wrapper = config.wrapper 
              if (wrapper && config.interactive) { 
                wrapper.addEventListener('mousemove'this._mouseMoveEventHandler.bind(this)) 
                wrapper.addEventListener('click'this._mouseClickEventHandler.bind(this)) 
              } 
            } 
           
            createBarrage(text: string, color: string, fontSize: string, left: string) { 
              if (this.domPool.length) { 
                const el = this.domPool.pop() 
                return _createBarrage(text, color, fontSize, left, el) 
              } else { 
                return _createBarrage(text, color, fontSize, left) 
              } 
            } 
           
            removeElement(target: HTMLElement) { 
              if (this.domPool.length < this.poolSize) { 
                this.domPool.push(target) 
                return 
              } 
              this.el.removeChild(target) 
            } 
           
            _mouseMoveEventHandler(e: Event) { 
              const target = e.target 
              if (!target) { 
                return 
              } 
           
              const newFreezeBarrage = this.elmToObj.get(target as HTMLElement) 
              const oldFreezeBarrage = this.freezeBarrage 
           
              if (newFreezeBarrage === oldFreezeBarrage) { 
                return 
              } 
           
              this.freezeBarrage = null 
           
              if (newFreezeBarrage) { 
                this.freezeBarrage = newFreezeBarrage 
                newFreezeBarrage.freeze = true 
                setHoverStyle(target as HTMLElement) 
                this.$emit('hover', newFreezeBarrage, target as HTMLElement) 
              } 
           
              if (oldFreezeBarrage) { 
                oldFreezeBarrage.freeze = false 
                const oldFreezeElm = this.objToElm.get(oldFreezeBarrage) 
                oldFreezeElm && setBlurStyle(oldFreezeElm) 
                this.$emit('blur', oldFreezeBarrage, oldFreezeElm) 
              } 
            } 
           
            _mouseClickEventHandler(e: Event) { 
              const target = e.target 
              const barrageObject = this.elmToObj.get(target as HTMLElement) 
              if (barrageObject) { 
                this.$emit('click', barrageObject, target) 
              } 
            } 
           
            reset() { 
              this.forEach(track => { 
                track.forEach(barrage => { 
                  const el = this.objToElm.get(barrage) 
                  if (!el) { 
                    return 
                  } 
                  this.removeElement(el) 
                }) 
                track.reset() 
              }) 
            } 

          彈幕池

          無(wú)法復(fù)制加載中的內(nèi)容 通過(guò)HTML+CSS實(shí)現(xiàn)的彈幕,每一個(gè)彈幕會(huì)對(duì)應(yīng)一個(gè) DOM 元素,為了減少頻繁的創(chuàng)建,會(huì)在屏幕的左側(cè)把上一輪已經(jīng)滾出舞臺(tái)的彈幕存到池子中,當(dāng)有新彈幕時(shí)會(huì)重新復(fù)用。

          軌道


          從我們平常見(jiàn)到的彈幕中可以看到,其實(shí)舞臺(tái)中間會(huì)存在多條平行的軌道,舞臺(tái)和軌道之間的關(guān)系是1對(duì)多的關(guān)系。當(dāng)彈幕運(yùn)行時(shí),依次渲染軌道中的彈幕。所以,軌道中會(huì)存在一個(gè)彈幕數(shù)組,代表著目前正在軌道上展示的彈幕;以及一個(gè)叫offset的變量,代表著目前軌道已被占據(jù)的寬度。

          class BarrageTrack<T extends BarrageObject
            barrages: T[] = [] 
            offset: number = 0 
           
            forEach(handler: TrackForEachHandler<T>) { 
              for (let i = 0; i < this.barrages.length; ++i) { 
                handler(this.barrages[i], i, this.barrages) 
              } 
            } 
           
            // 重置 
            reset() { 
              this.barrages = [] 
              this.offset = 0 
            } 
           
            // 加入新彈幕 
            push(...items: T[]) { 
              this.barrages.push(...items) 
            } 
           
            // 移除第一個(gè)(也就是剛剛出去的一個(gè)) 
            removeTop() { 
              this.barrages.shift() 
            } 
           
            remove(index: number) { 
              if (index < 0 || index >= this.barrages.length) { 
                return 
              } 
              this.barrages.splice(index, 1
            } 
           
            // 更新 Offset,只需要關(guān)注軌道中最后一個(gè)彈幕 
            updateOffset() { 
              const endBarrage = this.barrages[this.barrages.length - 1
              if (endBarrage) { 
                const { speed } = endBarrage 
                this.offset -= speed 
              } 
            } 

          碰撞

          彈幕的碰撞控制以及彈幕的呈現(xiàn)方式,其實(shí)全憑產(chǎn)品需求和個(gè)人喜好決定。以大多數(shù)彈幕為例,除了 B站的實(shí)現(xiàn)比較多樣化之外,更多的實(shí)現(xiàn)是通過(guò)平行軌道的方式實(shí)現(xiàn)。如果需要考慮彈幕的碰撞問(wèn)題,一般有兩種方法:

          1. 每個(gè)彈幕的速度都是相同的,所以也就不存在碰撞問(wèn)題,但是效果非常死板。
          2. 每個(gè)彈幕的速度都是不一樣的,但是需要解決碰撞問(wèn)題。

          為了實(shí)現(xiàn)不同的速度,最簡(jiǎn)單有效的方式其實(shí)就是通過(guò)『追及問(wèn)題』求出彈幕的最大速度。通過(guò)『追及問(wèn)題』,很容易求出彈幕B的最大速度 VB 。但是 VB 不應(yīng)該是彈幕的最終速度,考慮到距離 S 可能會(huì)比較大,那么 VB 的速度就會(huì)很大。于此同時(shí),應(yīng)該給彈幕的速度增加一點(diǎn)隨機(jī)性。因此,彈幕的速度比較好的呈現(xiàn)方式是:

          S = Math.max(VB, Random * DefaultSpeed) 


          DefaultSpeed 第一個(gè)彈幕在軌道上的默認(rèn)速度,它應(yīng)該根據(jù)實(shí)際需求設(shè)置成一個(gè)合適的值,然后 VB 的最大值不能超過(guò)它,不然的話彈幕只能在軌道上『一閃而過(guò)』。

          Demo

          https://logcas.github.io/a-barrage/example/css3.html 

          https://logcas.github.io/a-barrage/example/canvas.html

          參考資料

          https://w3c.github.io/danmaku/usecase.zh.html

          https://juejin.cn/post/6867689680670818317

          ?? 謝謝支持

          以上便是本次分享的全部?jī)?nèi)容,希望對(duì)你有所幫助^_^

          喜歡的話別忘了 分享、點(diǎn)贊、收藏 三連哦~。

          歡迎關(guān)注公眾號(hào) 前端Sharing 收貨大廠一手好文章~



          瀏覽 199
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲成人电影无码 | 激情内射网站 | 无码做爰欢H肉动漫网站在线看 | 久久爆乳一区二区三区 | 天天爽天天爽天天爽天天爽 |