vue-danmaku 其实是我发布的第一个 npm 包,不过发布以后就再也没管过了,因为当时写这个组件的目的主要是为了自用。

然而就在前几天!这个项目!竟然!被提了 issue!!!

这让我很震惊,毕竟是一个已经好久没有更新的“古老”项目了

溜去 npm 看了看,没想到竟然每周都有十几个下载量,虽然数量不多,可我还是慌的一匹……毕竟有人使用自己写的组件的感觉还是有些微妙。

所以写这篇文章的目的主要是补上自己迟到了几个月的开发总结。

开发前的思考

Github 上倒是有一些弹幕轮子库,然而这些轮子库都是基于时间模式的,换言之这些轮子只能用在视频或者音频上。但是显而易见,弹幕作为一种交流方式,在很多场景下并不具有时间属性,比如说有妖气上对于漫画的吐槽,起点上的小说章评(这里暂时不考虑弹幕对文字的阻挡 ˊ_>ˋ),甚至是一些活动页,活动方会将一些评论以弹幕的形式展现出来。(巧合的是我现在工作的工位旁边也有一台大屏,上面也会有弹幕的身影

这种形式在这几年越来越常见了,毕竟弹幕功能随着直播网站的兴起,已经从二次元扩展到了主流人群当中去。(这是另一个话题,我们暂且跳过)

所以开发一个非时间流式的弹幕交互组件已经是一个优先级很高的需求了(√

这个组件的特性如下:

  • 具备一个弹幕的基本属性
  • 非时间流式的
  • 可控制

可控制我想你们都不太明白这是什么意思,没关系这个概念也是我刚刚想出来的(强迫症表示凑够 3 条才开心

可控制:对于视频/音频/直播来说,弹幕都是会于某个关键帧处开始播放,也就是它是无法被组件所控制的,用户的输入时刻决定了它的播放时刻,组件只能控制弹幕的样式,并不能控制弹幕什么时候去展示。而非时间流式的组件需要控制弹幕播放的频率/速度/间隔,否则让弹幕一窝蜂出现不成 Ummmmm?

所以第一版的弹幕组件的需求确定如下:

  • 弹幕的滚动播放
  • 弹幕的暂停/继续/停止
  • 弹幕的显示/隐藏
  • 弹幕的出现频率
  • 弹幕的速度控制
  • 弹幕的字号控制
  • 弹幕的轨道数量控制
  • 插入弹幕功能

开发踩坑

站在事后诸葛亮的角度,我感觉这次开发过程中最难处理的其实只有三点:

  1. 绘制方式
  2. 插入方式
  3. 碰撞检测

上述三点决定了弹幕对 GPU、CPU 的性能影响,以及用户角度的显示效果。

绘制方式

网页上的动画有很多种实现方式:JS 动画、CSS 动画、SVG 动画、Canvas、WebGL、Web Animations 1.0 等等,对于弹幕来说可行的有 JS 动画、CSS 动画以及 Canvas。

使用 JS 动画,我们可以通过绝对定位+left+定时器的方式来实现弹幕的滚动,但是 left 的改变会产生 reflow(回流),大量 reflow 会导致移动端页面上的动画卡顿,因此非常不建议使用 JS 动画。

CSS 动画就显得轻松的多,相比 JS 动画,CSS 动画只会产生 repaint(重绘),重绘的成本比回流小的多,因为回流必然伴随着重绘,重绘却可以单独出现。(此处应当复习下回流与重绘)

Canvas 其实是比较优秀的动画实现方式,因为它很方便做比如暂停滚动等扩展功能,然而我 Canvas 写的不是很多,所以 Canvas 方案留待以后再做。

于是最终咱选择了 CSS 的 transform 来实现弹幕的滚动。

插入方式

由于没有时间属性,所以我们只要让弹幕顺序播放就可以了,这里我采用了从顶向下的展示方式。于是我们在 DOM 渲染之后获取到容器的高度和字体的高度,这时候就得到了弹幕的轨道数量。

然后我们将弹幕分别 push 到不同的轨道里面,开 N 个定时器轮询?(肯定不行,性能太差了

这里我非常想看看 bilibili 直播是怎么实现的,于是我熟练的打开了控制台开始翻起了代码,虽然 b 站压缩了代码,abcde 的变量很难搞懂是什么意思,不过还是大概明白了 b 站弹幕的渲染原理:客户端收到新的弹幕通知以后创建对应的元素,挂载到 DOM 上,当播放完毕以后销毁。(感觉跟没说一样)

顺便看了下斗鱼直播的弹幕实现方式,发现他们都是基于 transform 来实现绘制的,但是在弹幕的销毁上有一些区别:

b 站是给每个弹幕实例绑定了一个监听事件,通过监听 transitionend 来移除弹幕;而斗鱼是设置了一个定时器(定时器的时间大于弹幕的滚动时间),然后通过定时器来销毁弹幕(不知道吐槽什么,只能说虽然是个蠢办法,但是管用吧)

总之尝试了好几种方式,最终采用了这样的方案:

通过一个弹幕池对象来存储弹幕,数据和展现分离。新增弹幕的时候向弹幕池中增加一个弹幕元素,包括样式,速度等初始化的属性。然后用一个定时器不断扫描弹幕池,render 弹幕。(这样做即使以后需要将 render 从操作 dom 改成 canvas,改起来也会比较方便。)

碰撞检测

弹幕的展现方式目前有两种主流方案:从顶向下插入和随机高度插入。

不管是哪种展现方式都需要做弹幕的防重处理,简单说就是不要让弹幕重叠在一起,否则会看不清字。

由于没有想清楚逻辑,所以在这里折腾了两个晚上,最后实现想不出来,就跑去问了 DIYgod,他的解释是零界状态下弹幕运动到最左边的距离差。

恍然大悟,那么其实是要计算弹幕 1 运动到左侧和弹幕 2 运动到左侧的距离差值了,差值小于 0 的话,很明显就是必然会碰上的(给小鱼疯狂打 Call

所以碰撞检测的步骤如下:

  1. 从最上面一行开始检测,如果弹幕距离容器右侧的距离为负值则跳到下一行
  2. 如果该弹幕已经飞过本身的距离,且保留了一定安全距离,且如果第二条弹幕出现不会在终点追上弹幕 1,则插入当前弹幕到该行

优化

CSS3 的性能优化

为了开启 GPU 加速,最初选择了使用 translate3d 来代替 translateX。

-webkit-transform:transition3d(0,0,0) 或 -webkit-transform:translateZ(0),这两个属性都会开启 GPU 硬件加速模式,从而让浏览器在渲染动画时从 CPU 转向 GPU,其实说白了这是一个小伎俩,也可以算是一个 Hack,-webkit-transform:transition3d 和-webkit-transform:translateZ 其实是为了渲染 3D 样式,但我们设置值为 0 后,并没有真正使用 3D 效果,但浏览器却因此开启了 GPU 硬件加速模式。

以上方法是错误的!!!

以上方法是错误的!!!

以上方法是错误的!!!

重要的话说三遍,我们实际上不需要 z 轴的变化,但是还是假模假样地声明了,通过欺骗浏览器来达到目的,这其实是一种不人道的做法(´・_・`)

在这里我们应该使用 will-change,will-change 天生为此设计,顾名思意提前告诉浏览器元素要变化了。

此处应有科普:使用 CSS3 will-change 提高页面滚动、动画等渲染性能

暂停

最初的弹幕滚动是通过 transform: translateX() 来实现的,然后当我写到暂停功能的时候,我纠结该如何实现了。

于是我又熟练的打开了控制台跑去看 b 战的弹幕是如何实现暂停的(´・_,・`)

b 站的实现方案是这样:通过 JS 将 transition 的时间变为 0,当点击播放的时候重新计算当前屏幕上的弹幕距离左侧的距离,然后重新设置滚动时间。

虽然也能实现,但是感觉这样真的很麻烦,于是又去找了下其他弹幕网站的实现原理,最后感觉小鱼的 d-player 实现更加简单:animation。

animation 提供了一个属性:animation-play-state,而这个属性可以控制动画的播放状态。

性能测试

性能测试使用的是 stats.js,我在网页上渲染了 200 条弹幕进行测试,发现当弹幕同屏显示数量大于 8 行之后,帧率就开始逐渐降低了。

不过没有明显卡顿感,我想我应该找几台低配置的电脑去测试下。

被打断的 0.1.0 版本

其实本来是准备再加入一些基本功能以后迭代 0.1.0 版本的,然而当时自己正好辞职了,忙着准备找工作,于是开发就暂时停止,并且一拖就拖到了现在 ˊ_>ˋ

剩下的 TODO 事项其实还有很多,这里列一下,之后有空的时候继续开发吧。

  • [ ] 弹幕透明度
  • [ ] 弹幕颜色
  • [ ] 弹幕事件
  • [ ] 顶部悬停弹幕
  • [ ] 弹幕对象(每条弹幕应该有额外的属性用于存放弹幕的其他信息
  • [ ] 弹幕 slot(扩展需求,比如每条弹幕要加头像?