- 彈幕不重疊——核心訴求
- 場景簡單——彈幕勻速運(yùn)動
- 性能較好——避免密集的實時彈幕位置計算
針對這里的彈幕重疊問題(不妨說平面中的重疊問題)谁榜,我們可以從豎直方向和水平方向兩方面來分析逛薇。
豎直方向
對于典型的彈幕場景,每個彈幕元素作水平直線運(yùn)動外里,豎直方向(即縱向)的速度分量為0
邑飒。這就意味著:彈幕元素彼此之間在豎直方向沒有發(fā)生相對運(yùn)動,因此彈幕在縱向的間距可通過對容器劃分「軌道」進(jìn)行隔離级乐。
軌道好比公路上的單車道疙咸,每條單車道只允許一輛車通行。
我們可以使用相同的軌道高度(如下圖中h
)對彈幕的豎直方向進(jìn)行布局規(guī)劃风科。通過所在軌道編號
與軌道高度
的乘積計算撒轮,可獲得每條彈幕距離容器頂部的偏移量(top
值)乞旦。
原理上,軌道高度并不一定要大于彈幕高度题山,取較小的分度值也是沒有問題的兰粉。軌道可以是一個承載彈幕對象的隊列,本身并不需要展示為DOM
實體顶瞳。
水平方向
在上面的討論中玖姑,通過引入軌道的概念,避免了豎直方向上&不同軌道之間的彈幕重疊干擾慨菱。對于水平方向的分析焰络,我們不妨先從簡單的單個彈幕元素看起來。
每個彈幕元素會經(jīng)歷這樣三個階段:
- 出生在容器右側(cè)——掛載
- 進(jìn)入到容器中——運(yùn)動
-
完全離開容器——移除
在這里符喝,我們需要重新審視下運(yùn)動的主人公——彈幕君:每一條默默劃過的彈幕闪彼,實際上包含空間和時間兩個維度的描述:
空間維度:包括彈幕君的一些包括寬高、相對位置等幾何屬性协饲;
時間維度:
startTime 通常用于記錄用戶生成該彈幕的時刻畏腕,這是一個相對開始播放的時間偏移量。該屬性指導(dǎo)了彈幕元素的默認(rèn)出現(xiàn)時機(jī)茉稠,以及在候選隊列中的出場次序描馅。
duration,彈幕在容器中飄過的持續(xù)時間而线。由于彈幕場景中每個彈幕元素的水平位移量是固定的流昏,因此 duration 也間接決定了彈幕的運(yùn)動速度。
基于數(shù)據(jù)的視角吞获,一條彈幕運(yùn)動相關(guān)的信息可以表達(dá)如下:
// ts表述
interface bullet {
width : number, // 彈幕元素寬度
height : number, // 彈幕元素高度
top : number, // 彈幕元素距離容器頂部的偏移量
left : number, // 彈幕元素距離容器左邊緣的偏移量况凉,通常用于設(shè)定彈幕元素初始位置
startTime : number, // 彈幕元素相對于開始播放時刻的出現(xiàn)時機(jī)
duration: number, // 彈幕元素的展示持續(xù)時間
}
具象地說,這里的left
和top
指定了彈幕元素的出生點位各拷;width
和height
標(biāo)識了彈幕元素的高矮胖瘦刁绒;startTime
和duration
則分別決定了彈幕元素在候選列表中的順序和展示時長。
下面用原生 js 簡易描述一條彈幕運(yùn)動的配置:
// container 表示彈幕展示的容器
const containerPos = container.getBoundingClientRect();
const {
left: containerLeft,
width: containerWidth
} = containerPos;
// bullet 表示候選彈幕對象(下同)
const { width, right } = bullet.getBoundingClientRect();
// 計算彈幕移動速度
const moveV = (containerWidth + width) / duration;
// 彈幕需要移動的距離
const leftDistance = right - containerLeft;
// 彈幕剩余跑完時間(單位s)
const leftDuration = leftDistance / moveV;
// 彈幕參照容器定位
bullet.style.position = 'absolute';
// 彈幕出生點位(水平偏移量)
bullet.style.left = `${containerWidth}px`;
// pos是當(dāng)前彈幕元素被分配到的軌道編號烤黍,channelHeight表示軌道高度
bullet.style.top = `${pos * channelHeight}px`;
// 彈幕水平向左運(yùn)動
bullet.style.transition = `transform ${leftDuration}s linear 0s`;
bullet.style.transform = `translateX(-${right - containerLeft}px)`;
通過以上分析可知知市,由于容器和彈幕的寬高屬性是確定的,涉及彈幕的水平方向出生點位和速度也就被間接確定了速蕊。
這就意味著:一旦彈幕掛載到軌道中嫂丙,即可進(jìn)入預(yù)置的運(yùn)動狀態(tài)。
調(diào)度策略
設(shè)想规哲,如果存在一個調(diào)度策略跟啤,能夠智能地將彈幕分配至合適的軌道,在彈幕掛載階段已經(jīng)完成對彈幕重疊的檢測——那么就無需引入復(fù)雜的物理引擎或者是實時碰撞計算了,awesome~
那么隅肥,如何設(shè)計這個調(diào)度策略呢竿奏?
回到典型的彈幕場景:同一軌道中所有彈幕元素從右向左&同向勻速運(yùn)動;在展示期間腥放,軌道中所有彈幕元素均不發(fā)生重疊泛啸。
不難想到,「軌道中所有彈幕元素均不發(fā)生重疊」的問題可以歸約為:「如何避免軌道中前后兩個相鄰彈幕彈幕元素之間的重疊」秃症。
試想候址,軌道中前后彈幕元素之間互不重疊,那么整條軌道所有彈幕也就確保彼此不會重疊了种柑。例如上圖中岗仑,彈幕君-2與彈幕君-1、彈幕君-3與彈幕君-2在移動期間互不重疊莹规;對于即將進(jìn)入軌道的彈幕君-4赔蒲,只需要保證其與彈幕君-3保持移動期間不重疊泌神,則整個系統(tǒng)便可滿足互不重疊的狀態(tài)良漱。
于是我們成功地將一系列彈幕間的運(yùn)動關(guān)系,降維到了相鄰彈幕元素的兩兩關(guān)系欢际。
追及問題
判斷兩個彈幕在水平方向上是否發(fā)生重疊母市,實質(zhì)就是就是對追及問題進(jìn)行討論了。
基于紅領(lǐng)巾時代掌握的知識损趋,我們知道:對于兩個對象的勻速直線運(yùn)動患久,通過公式路程差 / 速度差 = 追及時間
來判斷對象是否會相遇(追及時間是否大于0
)。
這里的「相遇」浑槽,與彈幕場景中「重疊」的概念不謀而合蒋失。
實際操作中,對于一個尋求某條合適軌道的彈幕元素桐玻,只需要將其與軌道中最后加入的彈幕元素(即軌道中最右側(cè)的彈幕元素)進(jìn)行比較篙挽,通過兩者的追及問題計算,便可判斷該軌道是否滿足當(dāng)前候選彈幕的插入條件镊靴。
代碼簡單示意如下:
checkChannel() {
// containerWidth铣卡、containerLeft等,表示容器相關(guān)屬性
// bullet:表示候選彈幕元素對象
// channel:表示軌道偏竟,存儲進(jìn)入軌道的彈幕對象
const lastPos = channel.length - 1;
const lastBullet = channel[lastPos];
if (lastBullet) {
const lastBulletPos = lastBullet.getBoundingClientRect();
// 軌道中最后一個元素要求已經(jīng)全部進(jìn)入展示區(qū)域
if (lastBulletPos.right > containerRight) {
return false;
}
// 基本公式:s = v * t
const lastS = lastBulletPos.left - containerLeft + lastBulletPos.width;
const lastV = (containerWidth + lastBulletPos.width) / lastBullet.duration;
const lastT = lastS / lastV;
const newS = containerWidth;
const newV = (containerWidth + bullet.width) / bullet.duration;
const newT = newS / newV;
// 追及問題
if (lastV < newV && lastT > newT) {
return false;
}
}
return true;
}
經(jīng)過上面checkChannel()
的計算煮落,如果返回true
則表示當(dāng)前軌道可以接受候選彈幕的掛載。
最終彈幕元素是否能夠掛載到某條軌道上踊谋,還取決于其他因素:若彈幕本身高度較高&需要跨越多個軌道蝉仇,那么彈幕需要對相關(guān)干涉到的軌道進(jìn)行巡檢后,才能確定是否使用該軌道作為自己的掛載目標(biāo)。
綜上量淌,調(diào)度策略可簡要描述如下:
周期性巡檢軌道并嘗試插入候選彈幕元素骗村,檢查候選元素與軌道之間是否滿足掛載條件,包括:
- 溢出檢查:候選彈幕元素高度不超過所有軌道之和呀枢;
- 重復(fù)性檢查:候選彈幕元素尚未出現(xiàn)在軌道中胚股;
- 重疊檢查:候選彈幕元素,與所巡檢的軌道及其下方被占用的軌道(如果彈幕高度較高)中最右側(cè)的彈幕元素不發(fā)生重疊
滿足以上全部條件的軌道方可作為候選彈幕掛載裙秋。
小結(jié)
通過對平面中豎直和水平方向的分析琅拌,我們將寬泛的彈幕重疊問題,收斂為軌道中相鄰彈幕兩兩之間的追及問題摘刑,最終獲得了將候選彈幕掛載到合適軌道中的調(diào)度策略进宝。
在以上討論中,我們側(cè)重呈現(xiàn)一個問題分解的過程枷恕,并未過多地深入到代碼實現(xiàn)的細(xì)節(jié)党晋,希望能給到大家一些有益的啟發(fā)哈。
原文鏈接:https://www.zhihu.com/question/370464345/answer/1021530502