前端js实现弹幕,怎么控制弹幕不重叠

  • 弹幕不重叠——核心诉求
  • 场景简单——弹幕匀速运动
  • 性能较好——避免密集的实时弹幕位置计算

针对这里的弹幕重叠问题(不妨说平面中的重叠问题),我们可以从竖直方向水平方向两方面来分析。

竖直方向

对于典型的弹幕场景,每个弹幕元素作水平直线运动,竖直方向(即纵向)的速度分量为0。这就意味着:弹幕元素彼此之间在竖直方向没有发生相对运动,因此弹幕在纵向的间距可通过对容器划分「轨道」进行隔离。

轨道好比公路上的单车道,每条单车道只允许一辆车通行。

1111.jpg

我们可以使用相同的轨道高度(如下图中h)对弹幕的竖直方向进行布局规划。通过所在轨道编号轨道高度的乘积计算,可获得每条弹幕距离容器顶部的偏移量(top值)。

2222.jpg

原理上,轨道高度并不一定要大于弹幕高度,取较小的分度值也是没有问题的。轨道可以是一个承载弹幕对象的队列,本身并不需要展示为DOM实体。

水平方向

在上面的讨论中,通过引入轨道的概念,避免了竖直方向上&不同轨道之间的弹幕重叠干扰。对于水平方向的分析,我们不妨先从简单的单个弹幕元素看起来。

每个弹幕元素会经历这样三个阶段:

  1. 出生在容器右侧——挂载
  2. 进入到容器中——运动
  3. 完全离开容器——移除


    33333.jpg

在这里,我们需要重新审视下运动的主人公——弹幕君:每一条默默划过的弹幕,实际上包含空间时间两个维度的描述:

  • 空间维度:包括弹幕君的一些包括宽高、相对位置等几何属性;

  • 时间维度

  • startTime 通常用于记录用户生成该弹幕的时刻,这是一个相对开始播放的时间偏移量。该属性指导了弹幕元素的默认出现时机,以及在候选队列中的出场次序。

  • duration,弹幕在容器中飘过的持续时间。由于弹幕场景中每个弹幕元素的水平位移量是固定的,因此 duration 也间接决定了弹幕的运动速度。

基于数据的视角,一条弹幕运动相关的信息可以表达如下:

// ts表述
interface bullet {
    width   : number,   // 弹幕元素宽度
    height  : number,   // 弹幕元素高度
    top     : number,   // 弹幕元素距离容器顶部的偏移量
    left    : number,   // 弹幕元素距离容器左边缘的偏移量,通常用于设定弹幕元素初始位置
    startTime : number,   // 弹幕元素相对于开始播放时刻的出现时机
    duration: number,   // 弹幕元素的展示持续时间
}

具象地说,这里的lefttop指定了弹幕元素的出生点位;widthheight标识了弹幕元素的高矮胖瘦;startTimeduration则分别决定了弹幕元素在候选列表中的顺序和展示时长。

下面用原生 js 简易描述一条弹幕运动的配置:

// 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是当前弹幕元素被分配到的轨道编号,channelHeight表示轨道高度
bullet.style.top = `${pos * channelHeight}px`;

// 弹幕水平向左运动
bullet.style.transition = `transform ${leftDuration}s linear 0s`;
bullet.style.transform = `translateX(-${right - containerLeft}px)`;

通过以上分析可知,由于容器弹幕的宽高属性是确定的,涉及弹幕的水平方向出生点位和速度也就被间接确定了。

这就意味着:一旦弹幕挂载到轨道中,即可进入预置的运动状态。

调度策略

设想,如果存在一个调度策略,能够智能地将弹幕分配至合适的轨道,在弹幕挂载阶段已经完成对弹幕重叠的检测——那么就无需引入复杂的物理引擎或者是实时碰撞计算了,awesome~

那么,如何设计这个调度策略呢?

回到典型的弹幕场景:同一轨道中所有弹幕元素从右向左&同向匀速运动;在展示期间,轨道中所有弹幕元素均不发生重叠。

不难想到,「轨道中所有弹幕元素均不发生重叠」的问题可以归约为:「如何避免轨道中前后两个相邻弹幕弹幕元素之间的重叠」。

44444.jpg

试想,轨道中前后弹幕元素之间互不重叠,那么整条轨道所有弹幕也就确保彼此不会重叠了。例如上图中,弹幕君-2弹幕君-1弹幕君-3弹幕君-2在移动期间互不重叠;对于即将进入轨道的弹幕君-4,只需要保证其与弹幕君-3保持移动期间不重叠,则整个系统便可满足互不重叠的状态。

于是我们成功地将一系列弹幕间的运动关系,降维到了相邻弹幕元素的两两关系。

追及问题

判断两个弹幕在水平方向上是否发生重叠,实质就是就是对追及问题进行讨论了。

基于红领巾时代掌握的知识,我们知道:对于两个对象的匀速直线运动,通过公式路程差 / 速度差 = 追及时间来判断对象是否会相遇(追及时间是否大于0)。

这里的「相遇」,与弹幕场景中「重叠」的概念不谋而合。

实际操作中,对于一个寻求某条合适轨道的弹幕元素,只需要将其与轨道中最后加入的弹幕元素(即轨道中最右侧的弹幕元素)进行比较,通过两者的追及问题计算,便可判断该轨道是否满足当前候选弹幕的插入条件。

代码简单示意如下:

checkChannel() {
    // containerWidth、containerLeft等,表示容器相关属性
    // bullet:表示候选弹幕元素对象
    // channel:表示轨道,存储进入轨道的弹幕对象
    const lastPos = channel.length - 1;
    const lastBullet = channel[lastPos];
    if (lastBullet) {
        const lastBulletPos = lastBullet.getBoundingClientRect();

        // 轨道中最后一个元素要求已经全部进入展示区域
        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;   
}

经过上面checkChannel()的计算,如果返回true则表示当前轨道可以接受候选弹幕的挂载。

最终弹幕元素是否能够挂载到某条轨道上,还取决于其他因素:若弹幕本身高度较高&需要跨越多个轨道,那么弹幕需要对相关干涉到的轨道进行巡检后,才能确定是否使用该轨道作为自己的挂载目标。

综上,调度策略可简要描述如下:

周期性巡检轨道并尝试插入候选弹幕元素,检查候选元素与轨道之间是否满足挂载条件,包括:

  1. 溢出检查:候选弹幕元素高度不超过所有轨道之和;
  2. 重复性检查:候选弹幕元素尚未出现在轨道中;
  3. 重叠检查:候选弹幕元素,与所巡检的轨道及其下方被占用的轨道(如果弹幕高度较高)中最右侧的弹幕元素不发生重叠

满足以上全部条件的轨道方可作为候选弹幕挂载。

小结

通过对平面中竖直和水平方向的分析,我们将宽泛的弹幕重叠问题,收敛为轨道中相邻弹幕两两之间的追及问题,最终获得了将候选弹幕挂载到合适轨道中的调度策略。

在以上讨论中,我们侧重呈现一个问题分解的过程,并未过多地深入到代码实现的细节,希望能给到大家一些有益的启发哈。

原文链接:https://www.zhihu.com/question/370464345/answer/1021530502

你可能感兴趣的:(前端js实现弹幕,怎么控制弹幕不重叠)