fly-barrage 前端弹幕库(3):滚动弹幕的设计与实现

2024-03-01 23:52

本文主要是介绍fly-barrage 前端弹幕库(3):滚动弹幕的设计与实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

项目官网地址:https://fly-barrage.netlify.app/;
👑🐋🎉如果感觉项目还不错的话,还请点下 star 🌟🌟🌟。
Gitee:https://gitee.com/fei_fei27/fly-barrage(Gitee 官方推荐项目);
Github:https://github.com/feiafei27/fly-barrage;

其他系列文章:
fly-barrage 前端弹幕库(1):项目介绍
fly-barrage 前端弹幕库(2):弹幕内容支持混入渲染图片的设计与实现
fly-barrage 前端弹幕库(3):滚动弹幕的设计与实现

今天和大家说说滚动弹幕的设计与实现,为了便于理解,我会由简到难的一步步解析说明,主要包括以下几块内容:

  • 实现一条滚动弹幕的计算与渲染;
  • 实现多条滚动弹幕的计算与渲染;
  • 实现多条滚动弹幕相同字号并且不允许弹幕重叠的计算与渲染;
  • 实现多条滚动弹幕不同字号并且不允许弹幕重叠的计算与渲染;

1:实现一条滚动弹幕的计算与渲染

滚动弹幕具有如下两个特性:

export type BaseBarrageOptions = {// 弹幕的出现时间(毫秒为单位)time: number;
}
export type RenderConfig = {// 弹幕运行速度,仅对滚动弹幕有效(每秒多少像素)speed: number;
}

time 属性表示这个弹幕在视频播放器的时间是 time 的时候,这个滚动弹幕的左侧应该紧贴 Canvas 的右侧,此时弹幕的左侧距离 Canvas 的左侧距离正好是 Canvas 的宽,逻辑图如下所示:
在这里插入图片描述
speed 属性用于描述滚动弹幕的速度,意为每秒会移动多少像素。

每个滚动弹幕都有一个 originalLeft 属性,它标识着当播放进度是 0 的时候,滚动弹幕左侧距离 Canvas 左侧的距离,它的算法如下所示:

// 计算公式是:Canvas 元素的宽 + 弹幕出现时间 * 弹幕速度
this.originalLeft = this.br.canvasSize.width + (this.time / 1000) * this.br.renderConfig.speed;

如果某一个滚动弹幕的 time 属性为 0 的话,它的 originalLeft 正好等于 Canvas 的宽。

除了计算 originalLeft,还需要计算弹幕文本的宽,用于辅助计算某条滚动弹幕应不应该被 Canvas 实际渲染出来,弹幕宽度的计算方式可以看我的上一篇博客。

然后随着视频的播放,我们需要根据当前的播放进度计算出滚动弹幕左移的距离,计算方式如下所示:

// 弹幕整体向左移动的总距离,时间 * 速度
const translateX = (time / 1000) * this.br.renderConfig.speed;

左移的距离计算出来之后,我们就可以计算出这个滚动弹幕此时左侧距离 Canvas 左侧的距离,计算方式如下所示:

barrage.originalLeft - translateX

当滚动弹幕的左侧在 Canvas 右侧的左面并且滚动弹幕的右侧在 Canvas 左侧的右面的话,这个滚动弹幕就应该被渲染出来,借此我们可以计算出滚动弹幕是否会被渲染出来,计算方式如下所示:

const isShouldRender =// 弹幕的 originalRight 属性等于弹幕的 originalLeft 加上弹幕的宽度barrage.originalRight - translateX >= 0 &&barrage.originalLeft - translateX < this.br.canvasSize.width

如果 isShouldRender 变量为 true 的话,对弹幕进行绘制渲染操作即可(由于只有一条弹幕,所以暂不讨论弹幕渲染时的 top,随便设置一个 top 即可)。

2:实现多条滚动弹幕的计算与渲染

弹幕除了 left,还有 top 属性需要关注。在 B 站看视频,可以发现弹幕都是在固定的轨道中前后滚动的,就像现实生活中的高速公路一样,这样处理的好处是可以让播放的弹幕更加工整有层次,如果弹幕太乱的话,也会影响视频的浏览体验。

所以这里需要引入轨道的概念,借助轨道辅助计算弹幕的 top,让弹幕的滚动都是在轨道中。

轨道的高度等于滚动弹幕的高度,计算公式如下所示:

// 弹幕的高度等于弹幕字号乘以行高
const trackHeight = barrage.fontSize * barrage.lineHeight;

然后计算轨道的个数,计算公式如下所示:

// 计算总跑道数(canvas 高度 / 一个弹幕的高度)
const trackNum = Math.floor(canvas.height / trackHeight);

第 n 个轨道的 top 计算公式如下所示:

const n = 10;
const trackTop = (n - 1) * trackHeight;

然后我们遍历滚动弹幕,随机 push 进某一个轨道,将轨道的 top 设置给弹幕的 top 属性即可实现多条弹幕在轨道中滚动的效果。

3:实现多条滚动弹幕相同字号并且不允许弹幕重叠的计算与渲染(轨道算法)

当滚动弹幕的字号都是相同的时候,此时进行的不重叠计算是很简单的,计算步骤如下所示:

  1. 将所有的滚动弹幕按照 time 属性进行一次排序,time 小的排在前面;
  2. 遍历所有的滚动弹幕,判断当前循环的滚动弹幕能不能放到某一个实际轨道中,从上往下判断哪一个实际轨道能放进去;
  3. 如果当前判断的实际轨道中还没有弹幕或者当前实际轨道的最后一个弹幕和当前新增弹幕不重叠的话(是否会发生重叠可以通过两个滚动弹幕的 originalLeft 和 originalRight 计算出来 lastScrollBarrage.originalRight <= barrage.originalLeft),则表明当前循环的弹幕可以放到当前这个实际轨道中并且不会发生重叠的现象;
  4. 如果找到了能放进去的轨道的话,则将这个轨道的 top 设置给弹幕的 top 属性,并将当前这个弹幕的实例 push 到对应轨道的数组中即可;
  5. 如果没有找到能放进去的轨道的话,则不渲染当前这个弹幕,因为没有地方可以不重叠的渲染这个滚动弹幕;
  6. 至此滚动弹幕的不重叠 top 就计算出来了,再结合第一小节的知识进行渲染即可;

相关逻辑图如下所示:
初始阶段,所有轨道都为空:
初始阶段
第一个滚动弹幕能直接放到第一个轨道中:
第一个滚动弹幕
第二个滚动弹幕,和第一条轨道的最后一个弹幕有重叠,第二条轨道没有弹幕,所以放到了第二条轨道中:
第二个滚动弹幕
第三个滚动弹幕和第一个轨道的最后一个弹幕不重叠,所以能放到第一个轨道中:
第三个滚动弹幕
后面的弹幕按此逻辑逐个计算即可。

4:实现多条滚动弹幕不同字号并且不允许弹幕重叠的计算与渲染(虚拟轨道算法)

虚拟轨道算法的相关概念

实际轨道

这里的实际轨道和上面说的轨道是一个东西,只不过他的高度不能简单的视为 字号 x 行高,因为每个弹幕的字号和行高在极限情况下可以都是不同的。

这里规定实际轨道的高度是:所有弹幕字号 x 行高形成数字数组中的众数。

虚拟轨道

虚拟轨道是相邻实际轨道的合并,例如,我们现在有 4 个实际轨道,依次是:rt1、rt2、rt3、rt4,能够组成的虚拟轨道有 10 个分别是:
高度为 1 的:[rt1]、[rt2]、[rt3]、[rt4]
高度为 2 的:[rt1, rt2]、[rt2, rt3]、[rt3, rt4]
高度为 3 的:[rt1, rt2, rt3]、[rt2, rt3, rt4]
高度为 4 的:[rt1, rt2, rt3, rt4]
每个虚拟轨道都有一个对应的数组,用于存放弹幕,弹幕只能存放到虚拟轨道中,不要放到实际轨道中。

计算过程

  1. 计算弹幕高度,判断它适合放在高度为几的虚拟轨道,假设它有两个实际轨道的高度,那么它应该放在高度为2的虚拟轨道;
  2. 遍历高度为 2 的 3 个虚拟轨道,判断有没有虚拟轨道能放下,这里先判断 [rt1, rt2] 能不能放下,能放下的条件是:所有包含 rt1 或者 rt 2 两个实际轨道的虚拟轨道的最后一个弹幕和当前新增弹幕不重叠,如果满足这个条件的话,则 [rt1, rt2] 能放下,否则放不下,继续判断下一个虚拟轨道 [rt2, rt3] 能不能放下,如果高度为 2 的 3 个虚拟轨道都遍历完了,还没有的话,就说明当前新增弹幕无法不重叠的放入;
  3. 如果能够找到不重叠虚拟轨道的话,根据虚拟轨道计算 top,并将当前的弹幕实例 push 到对应虚拟轨道对应的数组中;

相关逻辑图如下所示:
初始阶段,所有轨道都为空:
初始阶段,所有轨道都为空
第一个弹幕的高度是2个实际轨道,能放到 [rt1, rt2]:
第一个弹幕的高度是2个实际轨道,能放下
第二个弹幕的高度是1个实际轨道, [rt1] 和 [rt2] 都放不下,因为第一个弹幕 [rt1, rt2] 占据了这 2 个实际轨道,不过能放到 [rt3]:
放失败的场景
放成功的场景
第三个弹幕的高度是3个实际轨道,它哪个虚拟轨道都放不下:
在这里插入图片描述
后续的新弹幕逻辑以此类推,对应的代码实现如下所示:

/*** 进行不允许重叠的布局* @param scrollBarrages 滚动弹幕实例数组*/
avoidOverlapLayout(scrollBarrages: ScrollBarrage[]) {const startTime = Date.now();// 将所有虚拟轨道中的存储弹幕清空this.virtualTracks.forEach(vt => vt.clearBarrage());// 遍历进行布局计算scrollBarrages.forEach(barrage => {// 遍历 grade 属性为 grade 的虚拟轨道,判断能不能放进去const fittedVirtualTracks = this.gradeToVtsMap.get(barrage.grade) || [];for (let i = 0; i < fittedVirtualTracks.length; i++) {// 当前遍历的 virtualTrackconst virtualTrack = fittedVirtualTracks[i];// 判断当前遍历的 virtualTrack 能不能放进去;// 能放进去的条件是:包含 virtualTrack 内部任一实际轨道的虚拟轨道(grade <= maxGrade)的最后一个弹幕和当前新增弹幕都不重叠const canPush = (this.vtToVtsMap.get(virtualTrack) || []).every(item => {// 获取最后一个滚动弹幕const lastScrollBarrage = item.getLastBarrage();// 如果当前轨道没有滚动弹幕的话,说明和最后一个弹幕不可能重叠,直接 return true 即可if (!lastScrollBarrage) return true;// 有的话,判断是否会重叠return lastScrollBarrage.originalRight + this.minSpace <= barrage.originalLeft;});if (canPush) {barrage.show = true;virtualTrack.push(barrage);// 计算该滚动弹幕的 topbarrage.top = virtualTrack.top;break;} else {barrage.show = false;}}// 重要的弹幕,如果没有虚拟轨道能插进去的话,则随机一个实际轨道if (barrage.prior && !barrage.show) {this.randomTrackBarrage(barrage);}});this.isLogKeyData && console.log(`虚拟轨道算法花费时间:${(Date.now() - startTime)}ms`);
}

这篇关于fly-barrage 前端弹幕库(3):滚动弹幕的设计与实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/764253

相关文章

分布式锁在Spring Boot应用中的实现过程

《分布式锁在SpringBoot应用中的实现过程》文章介绍在SpringBoot中通过自定义Lock注解、LockAspect切面和RedisLockUtils工具类实现分布式锁,确保多实例并发操作... 目录Lock注解LockASPect切面RedisLockUtils工具类总结在现代微服务架构中,分布

Java使用Thumbnailator库实现图片处理与压缩功能

《Java使用Thumbnailator库实现图片处理与压缩功能》Thumbnailator是高性能Java图像处理库,支持缩放、旋转、水印添加、裁剪及格式转换,提供易用API和性能优化,适合Web应... 目录1. 图片处理库Thumbnailator介绍2. 基本和指定大小图片缩放功能2.1 图片缩放的

Python使用Tenacity一行代码实现自动重试详解

《Python使用Tenacity一行代码实现自动重试详解》tenacity是一个专为Python设计的通用重试库,它的核心理念就是用简单、清晰的方式,为任何可能失败的操作添加重试能力,下面我们就来看... 目录一切始于一个简单的 API 调用Tenacity 入门:一行代码实现优雅重试精细控制:让重试按我

Redis客户端连接机制的实现方案

《Redis客户端连接机制的实现方案》本文主要介绍了Redis客户端连接机制的实现方案,包括事件驱动模型、非阻塞I/O处理、连接池应用及配置优化,具有一定的参考价值,感兴趣的可以了解一下... 目录1. Redis连接模型概述2. 连接建立过程详解2.1 连php接初始化流程2.2 关键配置参数3. 最大连

Python实现网格交易策略的过程

《Python实现网格交易策略的过程》本文讲解Python网格交易策略,利用ccxt获取加密货币数据及backtrader回测,通过设定网格节点,低买高卖获利,适合震荡行情,下面跟我一起看看我们的第一... 网格交易是一种经典的量化交易策略,其核心思想是在价格上下预设多个“网格”,当价格触发特定网格时执行买

python设置环境变量路径实现过程

《python设置环境变量路径实现过程》本文介绍设置Python路径的多种方法:临时设置(Windows用`set`,Linux/macOS用`export`)、永久设置(系统属性或shell配置文件... 目录设置python路径的方法临时设置环境变量(适用于当前会话)永久设置环境变量(Windows系统

Mysql中设计数据表的过程解析

《Mysql中设计数据表的过程解析》数据库约束通过NOTNULL、UNIQUE、DEFAULT、主键和外键等规则保障数据完整性,自动校验数据,减少人工错误,提升数据一致性和业务逻辑严谨性,本文介绍My... 目录1.引言2.NOT NULL——制定某列不可以存储NULL值2.UNIQUE——保证某一列的每一

Python对接支付宝支付之使用AliPay实现的详细操作指南

《Python对接支付宝支付之使用AliPay实现的详细操作指南》支付宝没有提供PythonSDK,但是强大的github就有提供python-alipay-sdk,封装里很多复杂操作,使用这个我们就... 目录一、引言二、准备工作2.1 支付宝开放平台入驻与应用创建2.2 密钥生成与配置2.3 安装ali

Spring Security 单点登录与自动登录机制的实现原理

《SpringSecurity单点登录与自动登录机制的实现原理》本文探讨SpringSecurity实现单点登录(SSO)与自动登录机制,涵盖JWT跨系统认证、RememberMe持久化Token... 目录一、核心概念解析1.1 单点登录(SSO)1.2 自动登录(Remember Me)二、代码分析三、

PyCharm中配置PyQt的实现步骤

《PyCharm中配置PyQt的实现步骤》PyCharm是JetBrains推出的一款强大的PythonIDE,结合PyQt可以进行pythion高效开发桌面GUI应用程序,本文就来介绍一下PyCha... 目录1. 安装China编程PyQt1.PyQt 核心组件2. 基础 PyQt 应用程序结构3. 使用 Q