您的当前位置:首页正文

HTML如何利用canvas实现弹幕功能

2020-11-27 来源:客趣旅游网
本篇文章主要介绍HTML如何利用canvas实现弹幕功能,感兴趣的朋友参考下,希望对大家有所帮助。

简介

最近在做大作业的时候需要做一个弹幕播放器。借鉴了一下别人的源码自己重新实现了一个,演示如下

主要的功能有

发送弹幕
设置弹幕的颜色,速度和类型
显示弹幕

已知缺陷:

不能全屏

canvas没有做自适应
没有自定义播放器控件
没有根据播放时间显示相应的弹幕
弹幕不能实现悬停
已知的缺陷以后会进行改进。网上能找到的弹幕播放器的源码一般只做了滚动的弹幕而没有做静止的弹幕,这里我特意加上了静止弹幕的实现。

Canvas绘制文字以及文字滚动效果

整个播放器的核心就是绘制文字以及做文字滚动的动画,canvas中对于文字并没有很好的动画支持,只能通过自己实现,实现的思路就是不断的清屏然后重写文字,当清屏重写的频率达到24fps的时候就是流畅的动画了。

先在HTML文件中添加视频video标签和画布canvas标签

<p id="barrageplayer">
 <canvas id="cv_video" width="900px" height="450px"></canvas>
 <video id="v_video" src="test.MP4" controls type="video/mp4"></video>
</p>

把canvas标签的位置样式设置为position:absolute然后视频和画布就重叠在一起,看起来就是一个弹幕播放器了。然后为画布添加弹幕相关的内容,首先获取画布的相关信息和设置画布的字体大小和字体样式

var c=document.getElementById("cv_video");
//获取画布大小
var c_height=c.height;
var c_width=c.width;
//获取画布
ctx=c.getContext("2d");
//设置字体样式
ctx.font="25px DengXian";
画布信息已经获取和设置,巧妇难为无米之炊,接着我们就要构造弹幕对象,使用的构造模式是动态原型模式
//弹幕对象
function Barrage(content,color,type,speed){
 this.content=content;
 this.color=color;
 this.type=type;
 this.speed=speed;
 if(this.type=="default"){
 this.height=parseInt(Math.random()*c_height)+10;
 }else if (this.type=="static top"){
 this.height=parseInt((c_height/2)-Math.random()*c_height/2)+10;
 }else if (this.type=="static bottom"){
 this.height=parseInt((c_height/2)+Math.random()*c_height/2)+10;
 }
 if(typeof this.move!="function"){
 Barrage.prototype.move=function(){
 if(this.type=="default"){
 this.left=this.left-this.speed;
 }
 }
 }
}

构造的弹幕对象初始化了各种参数,包括内容,颜色,运动类型和速度,定义了move()方法来控制弹幕的缓动,每出发一次move()方法向左滚动一个单位speed的像素。
弹幕对象构造完成之后就进入到主题,动画的制作,直接上代码

//循环擦写画布实现动画效果
setInterval(function(){
 ctx.clearRect(0,0,c_width,c_height);
 ctx.save();
 for(var i=0;i<msgs.length;i++){
 if(msgs[i]!=null){
 if(msgs[i].type=="default"){
 handleDefault(msgs[i]);
 }else{
 handleStatic(msgs[i]);
 }
 }
 }
},20)

每20ms执行一次擦写,ctx.clearRect(0,0,c_width,c_height);是将整张当前的画布清除,然后使用ctx.save()将当前的画布保存,接着遍历弹幕列表(msgs是弹幕列表,当每发送一条弹幕都会将该弹幕实例添加到列表中),然后按照默认样式的弹幕还是静止样式的弹幕分别处理。如果是默认样式的弹幕将会按照以下的方法处理

//处理默认弹幕样式
function handleDefault(barrage){
 if(barrage.left==undefined||barrage.left==null){
 barrage.left=c.width;
 }else{
 if(barrage.left<-200){
 barrage=null;
 }else{
 barrage.move()
 ctx.fillStyle=barrage.color;
 ctx.fillText(barrage.content,barrage.left,barrage.height)
 ctx.restore();
 }
 } 
}

首先如果弹幕实例没有设置left属性则将画布的宽度赋予它,如果弹幕实例已经退出画布则将其置null以节省内存,否则的话就调用弹幕实例的move()方法改变left属性的值,然后设置文字的颜色,一级写入新的文字,恢复画布。这样就完成了一帧动画。

对于静止弹幕的实现方法如下

//处理静止弹幕样式
function handleStatic(barrage){
 ctx.moveTo(c_width/2,barrage.height);
 ctx.textAlign="center";
 ctx.fillStyle=barrage.color;
 ctx.fillText(barrage.content,c_width/2,barrage.height);
 if(barrage.left==undefined||barrage.left==null){
 barrage.left=c.width;
 }else{
 if(barrage.left<-200){
 ctx.fillText("",c_width/2,barrage.height); 
 barrage=null;
 //ctx.restore();
 ctx.clearRect(0,0,c_width,c_height); 
 }else{
 barrage.left=barrage.left-6;
 }
 }
}

首先将画布的基点移动到画布的中心,需要注意的是这时候相对与生成了一张新的画布,原来画布的clearRect()方法已经不适用与这张画布了。然后再设置文字对齐为居中对齐,设置文字样式,填充文字。因为弹幕是静止的所以不需要进行缓动,但是静止弹幕也是会消失的,需要设置一个标志位来使他定时消失。在这里为了不占用额外的属性,我们直接使用left属性作为标志位,同样进行left属性的递减,但不把递减反映到画布中,当left达到阈值,则使用ctx.clearRect()方法将弹幕清除。这样就实现了静止弹幕的处理。

其他关于颜色,样式的设置有一定基础的人应该是很容易掌握的在这里就不多介绍了,自己看可运行代码部分理解一下就好。

总结

这个项目主要是使用了canvas进行文字绘制以及实现文字的缓动动画,主要用到的方法有

canvasDom.getContext()
canvas.save()/canvas.restore()
canvas.clearRect()
canvas.moveTo()

原来我对与save()和restore()是不能理解的,现在我算是有一点理解了,当你更改了画布状态,现在的画布就已经不是原来的画布,所以在修改画布状态之前先把画布状态保存,切换画布状态,完成工作之后,恢复为原来的画布状态继续工作。像我处理静态弹幕的时候,把画布的基点改变了,那么原来画布的清除方法就不再适用于当前画布,只有在当前画布中自己使用另外的清除方法。然后再恢复到原来的画布。

可运行代码

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <meta http-equiv="X-UA-Compatible" content="ie=edge">
 <title>Document</title>
</head>
<style type="text/css">
 .pickp{
 width: 30px;
 height: 30px;
 cursor: pointer;
 border: 2px solid gray;
 display: inline-block;
 }
 #white{
 background: white;
 }
 #red{
 background:#ff6666;
 }
 #yellow{
 background:#ffff00;
 }
 #blue{
 background:#333399;
 }
 #green{
 background:#339933;
 }
 #cv_video{
 position: absolute;
 z-index: 1;
 }
 #barrageplayer{
 position: relative;
 display: block;
 width: 900px;
 height: 500px;
 }
 #v_video{
 position: absolute;
 width: 100%;
 height: 100%;
 z-index: 0;
 }
</style>
<body>
 <p id="barrageplayer">
 <canvas id="cv_video" width="900px" height="450px"></canvas>
 <video id="v_video" src="test.MP4" controls type="video/mp4"></video>
 </p>
 <p id="barrageinput">
 <p>
 <input type="text" id="smsg" placeholder="请输入弹幕内容"/>
 <button id="send"> 发送</button>
 </p>
 <p id="colorpick">
 <p class="pickp" id="white"></p>
 <p class="pickp" id="red"></p>
 <p class="pickp" id="yellow"></p>
 <p class="pickp" id="blue"></p>
 <p class="pickp" id="green"></p>
 </p>
 <p id="typepick">
 <input type="radio" name="type" value="default">默认
 <input type="radio" name="type" value="static top">静止顶部 
 <input type="radio" name="type" value="static bottom">静止底部
 </p>
 <p id="speedpick">
 <input type="radio" name="speed" value="1">1X
 <input type="radio" name="speed" value="2">2X
 <input type="radio" name="speed" value="3">3X
 </p>
 <p id="stylepick"></p>
 </p>
 <script>
 var c=document.getElementById("cv_video");
 var typeDom=document.getElementsByName("type");
 var speedDom=document.getElementsByName("speed");
 var colorpick=document.getElementById("colorpick");
 var smsg=document.getElementById("smsg");
 var color="#white";
 var speed=1;
 var type="default";
 var msgs=[];
 //获取画布大小
 var c_height=c.height;
 var c_width=c.width;
 //获取画布
 ctx=c.getContext("2d");
 ctx.font="25px DengXian";
 //处理颜色选择
 colorpick.addEventListener('click',function(event){
 switch(event.target.id){
 case "white":
 color="white";
 break;
 case "red":
 color="#ff6666";
 break;
 case "yellow":
 color="#ffff00";
 break;
 case "green":
 color="#339933";
 break;
 case "blue":
 color="#333399";
 break;
 }
 })
 //处理发送弹幕
 document.getElementById("send").onclick=function(){
 var text=smsg.value;
 for(var i=0;i<typeDom.length;i++){
 if(typeDom[i].checked){
 type=typeDom[i].value;
 break;
 }
 }
 for(var i=0;i<speedDom.length;i++){
 if(speedDom[i].checked){
 speed=2*parseInt(speedDom[i].value);
 break;
 }
 }
 var tempBarrage=new Barrage(text,color,type,speed);
 msgs.push(tempBarrage);
 }
 //
 //弹幕功能部分代码
 //
 //弹幕对象
 function Barrage(content,color,type,speed){
 this.content=content;
 this.color=color;
 this.type=type;
 this.speed=speed;
 if(this.type=="default"){
 this.height=parseInt(Math.random()*c_height)+10;
 }else if (this.type=="static top"){
 this.height=parseInt((c_height/2)-Math.random()*c_height/2)+10;
 }else if (this.type=="static bottom"){
 this.height=parseInt((c_height/2)+Math.random()*c_height/2)+10;
 }
 if(typeof this.move!="function"){
 Barrage.prototype.move=function(){
 if(this.type=="default"){
 this.left=this.left-this.speed;
 }
 }
 }
 }
 //循环擦写画布实现动画效果
 setInterval(function(){
 ctx.clearRect(0,0,c_width,c_height);
 ctx.save();
 for(var i=0;i<msgs.length;i++){
 if(msgs[i]!=null){
 if(msgs[i].type=="default"){
 handleDefault(msgs[i]);
 }else{
 handleStatic(msgs[i]);
 }
 }
 }
 },20)
 //处理默认弹幕样式
 function handleDefault(barrage){
 if(barrage.left==undefined||barrage.left==null){
 barrage.left=c.width;
 }else{
 if(barrage.left<-200){
 barrage=null;
 }else{
 barrage.move()
 ctx.fillStyle=barrage.color;
 ctx.fillText(barrage.content,barrage.left,barrage.height)
 ctx.restore();
 }
 } 
 }
 //处理静止弹幕样式
 function handleStatic(barrage){
 ctx.moveTo(c_width/2,barrage.height);
 ctx.textAlign="center";
 ctx.fillStyle=barrage.color;
 ctx.fillText(barrage.content,c_width/2,barrage.height);
 if(barrage.left==undefined||barrage.left==null){
 barrage.left=c.width;
 }else{
 if(barrage.left<-200){
 ctx.fillText("",c_width/2,barrage.height); 
 barrage=null;
 //ctx.restore();
 ctx.clearRect(0,0,c_width,c_height); 
 }else{
 barrage.left=barrage.left-6;
 }
 }
 }
 </script>
</body>
</html>

总结:

显示全文