主题
Canvas实现弹幕功能
效果展示
前置知识
动画原理
需求 假设在 60Hz 的屏幕下,我们想要实现一个 div 从左->右移动 500px ,并且 div 平滑移动,而不是一下到达终点。
实现思路 60Hz 的屏幕,意味着每 16.7ms 屏幕就会更新图像,如果我们在屏幕每次刷新图像前,将 div 元素向右移动 1px,这样的话,每次屏幕刷新的时候,我们的 div 元素都向右移动了一点点位置。但是由于间隔时间太短,我们的大脑和眼睛是无法处理的,所以我们会误认为 div 是平滑移动的。
所以我们的动画原理其实很简单,就是在很短的时间内不断更新相同间隔或者相同差别的图片,给我们人眼造成连续的假象。 我们常说的 60 帧电影,其实就是每秒 60 刷新 60 次。
前端实现动画的方式
- css animation https://developer.mozilla.org/zh-CN/docs/Web/CSS/animation 能用css实现的动画就不要用js
css
<style>
div {
position: fixed;
left: 0;
width: 100px;
height: 100px;
background-color: pink;
/* 使用 */
animation: move linear 5s;
}
/* 定义动画 */
@keyframes move {
form { left: 0px; }
to { left: 500px; }
}
</style>
- 定时器 setInterval 由于是异步的,定时器回调函数的代码会放入异步队列,因此执行时机不准确。 即使页面或浏览器窗口隐藏(页面暂停渲染),定时器仍然在跑,浪费性能。
html
<style>
div {
position: fixed;
left: 0;
width: 100px;
height: 100px;
background-color: pink;
}
</style>
<script>
var div = document.querySelector('div');
let left = 0
var move = setInterval(() => {
left ++;
div.style.left = left + 'px'
if (left >= 500) clearInterval(move)
}, 16.6)
</script>
- window.requestAnimationFrame 该API能以浏览器的显示频率来作为其动画动作的频率,比如浏览器每10ms刷新一次,动画回调也每10ms调用一次 窗口或页面隐藏,会暂停调用函数, 页面显示后又接着调用
html
<style>
div {
position: fixed;
left: 0;
width: 100px;
height: 100px;
background-color: pink;
}
</style>
<script>
var div = document.querySelector('div');
let left = 0
function move() {
left++
div.style.left = left + 'px'
if (left < 500) window.requestAnimationFrame(move)
}
move()
</script>
画布 canvas
HTML5 的 canvas 元素使用 JavaScript 在网页上绘制图像。 画布是一个矩形区域,您可以控制其每一像素。 canvas 拥有多种绘制路径、矩形、圆形、字符以及添加图像的方法。
API https://www.w3school.com.cn/tags/html_ref_canvas.asp
弹幕轨迹原理解析
实现流程
1. Barrage类
作用: 用于初始化每一条弹幕,包括弹幕属性(颜色,字体大小,出现位置,出现时机,速度
js
class Barrage {
constructor(barrage, ctx) {
this.barrage = barrage;
this.ctx = ctx;
this.text = barrage.text;
this.time = barrage.time;
}
// 初始化当前这个弹幕,
init() {
// 为这条弹幕添加一些属性
this.color = this.barrage.color || this.ctx.color;
this.fontSize = this.barrage.fontSize || this.ctx.fontSize;
this.speed = this.barrage.speed || this.ctx.speed;
// 求出 当前这条弹幕的长宽
let span = document.createElement("span");
span.innerText = this.text;
span.style.font = this.fontSize + "px Microsoft YaHei";
span.style.position = "absolute";
document.body.appendChild(span);
this.width = span.clientWidth; // 该条弹幕宽度
this.height = span.clientHeight; // 该条弹幕高度
document.body.removeChild(span);
// 弹幕出现的位置
this.x = this.ctx.canvas.width;
// 注意,canvas里面文字的y是文字的基线,而不是左上角顶点
this.y = this.ctx.canvas.height * Math.random();
// 弹幕是都继续在画布上渲染
if (this.y <= this.height) {
// 不让文字超出canvas上边框
this.y = this.height;
}
if (this.y > this.ctx.canvas.height) {
// 不让文字超出canvas下边框
this.y = this.ctx.canvas.height;
}
}
// 将当前这个弹幕渲染到屏幕上
render() {
this.ctx.contxt.font = `${this.fontSize}px Microsoft YaHei`;
this.ctx.contxt.fillStyle = this.color;
this.ctx.contxt.fillText(this.text, this.x, this.y);
}
}
2. Canvas类
作用:控制整个画布上的每一条弹幕
js
// 用于控制整个canvas上的弹幕
class Canvas {
constructor(canvas, video, options) {
if (!canvas || !video) {
// 若没有canvas和视频播放器,则不进行以后的所有操作
return;
}
this.canvas = canvas;
this.video = video;
this.options = options;
this.canvas.width = video.clientWidth;
this.canvas.height = video.clientHeight;
this.contxt = canvas.getContext("2d"); // canvas上下文
// 是否暂停, 默认是暂停
this.isSuspend = true;
// 设置弹幕默认属性,但是弹幕内容和弹幕出现的时间是必须给的,其余属性有就用,没有就取默认值
let defaultOpt = {
color: "red", // 默认颜色
speed: 2, // 移动速度
fontSize: 20, // 字号
data: []
};
// 合并对象 并挂在在实例上面
Object.assign(this, defaultOpt, options);
// 用于存放所有弹幕
this.allBarrage = this.options.data.map(
barrage => new Barrage(barrage, this)
);
// 渲染弹幕
this.render();
}
// 渲染弹幕
render() {
// 先清空画布
this.contxt.clearRect(0, 0, this.canvas.width, this.canvas.height);
// 渲染弹幕
this.renderBarrage();
// 如果当前是播放状态则开始递归渲染
if (this.isSuspend === false) {
requestAnimationFrame(this.render.bind(this));
}
}
// 真正的渲染方法
renderBarrage() {
// 若当前视频的时间大于该条弹幕该出现的时间,则渲染该条弹幕
// 1. 拿到当前视频播放的时间
let currentTime = this.video.currentTime;
this.allBarrage.forEach(barrabeItem => {
/* barrabeItem是每个弹幕的实例 */
if (!barrabeItem.flag && currentTime >= barrabeItem.time) {
// 渲染之前,若没有初始化该条弹幕则先初始化该条弹幕
if (!barrabeItem.isInit) {
barrabeItem.init();
barrabeItem.isInit = true;
}
// 更新位置
barrabeItem.x -= barrabeItem.speed;
barrabeItem.render(); // 渲染自己到画布上
// 当弹幕移动出了canvas,则停止渲染
if (barrabeItem.x <= -barrabeItem.width) {
barrabeItem.flag = true;
}
}
});
}
// 添加弹幕(用于输入框添加弹幕,并将弹幕画在画布上)
addBarrage(barrage) {
this.allBarrage.push(new Barrage(barrage, this));
}
// 重置弹幕(用于进度条拖动时候控制弹幕是否该出现)
reset() {
this.contxt.clearRect(0, 0, this.canvas.width, this.canvas.height); // 先清除所有弹幕
// 拿到当前视频的时间
let currentTime = this.video.currentTime;
// 让每一个弹幕回到初始值
this.allBarrage.forEach(item => {
item.flag = false;
// 若当前视频时间小于弹幕出现时间,则弹幕还应该出现,所以重新初始化弹幕
if (currentTime <= item.time) {
item.isInit = false;
} else {
// 否则不该出现
item.flag = true;
}
});
}
}
3. 一些必要的事件
js
/* 弹幕池 */
const data = [];
/* 控制画布的实例 */
const canvasBarrage = new Canvas(canvasEle, videoEle, { data: arg });
/* 播放视频时候开始播放弹幕 */
videoEle.addEventListener("play", function(e) {
canvasBarrage.isSuspend = false;
canvasBarrage.render();
});
/* 暂停视频时候暂停弹幕 */
videoEle.addEventListener("pause", function(e) {
canvasBarrage.isSuspend = true;
});
/* 拖动进度条重置弹幕 */
videoEle.addEventListener("seeked", function(e) {
canvasBarrage.reset();
});
/* 往画布上添加弹幕 */
let obj = {
text: "xxxxxx",
time: videoEle.currentTime,
color: colorEle.value,
fontSize: rangeEle.value
};
canvasBarrage.addBarrage(obj);