Web编程大作业(五)在线黑板&弹幕功能(canvas+websocket)
在线黑板
前置准备
先用canvas做一个简易版的黑板,能改变笔触的颜色和粗细,并将当前绘图复制到另一个元素上
这个简易版黑板的代码也可以看一下
<style>
.can{
border: 2px black solid;
top:100px;
margin:auto;
}
#show{
border: 2px black solid;
top:100px;
height:500px;
width:500px;
left:0px;
}
</style>
<script>
var color='black';
var width=5;
var x1,y1,x2,y2;
var isMouseDown=false;
var canvas;
var context;
var container;
window.onload=function(){
canvas=document.getElementById('canvas');
context=canvas.getContext("2d");
container=document.getElementById('container');
container.onmousedown=mouseDownAction;
document.onmouseup=mouseUpAction;
canvas.onmousemove=mouseMoveAction;
console.log(canvas,context);
}
function mouseDownAction(e){
isMouseDown=true;
// console.log(e);
x1=e.offsetX;
y1=e.offsetY;
console.log(x1,y1);
}
function mouseUpAction(){
isMouseDown=false;
}
function mouseMoveAction(e){
if (isMouseDown){
x2=e.offsetX;
y2=e.offsetY;
drawLine(x1,y1,x2,y2);
x1=x2;
y1=y2;
}
}
function drawLine(x1,y1,x2,y2){
context.beginPath();
context.moveTo(x1,y1);
context.lineWidth=width;
context.strokeStyle=color;
context.lineTo(x2,y2);
console.log(color,width);
context.stroke();
}
function showRange(){
console.log(document.getElementById('width').value);
width=document.getElementById('width').value/10;
}
function showColor(){
console.log(document.getElementById('color').value);
color=document.getElementById('color').value;
}
function broadcast(){
console.log(canvas.toDataURL("image/png",0.92));
var show=document.getElementById('show');
show.innerHTML="<img src='"+canvas.toDataURL("image/png",0.92)+"'>'"
}
function clearCan(){
context.clearRect(0,0,1000,1000);
}
</script>
<body>
<div class='center' id="container">
<canvas class='can' id="canvas" width="500" height="500"></canvas>
</div>
<input type="color" id="color" onchange="showColor()">选择颜色
<input type="range" id="width" onchange="showRange()">选择笔触
<button onclick="broadcast()">广播</button>
<button onclick="clearCan()">清空</button>
<div id='show'></div>
</body>
实现原理
原理就是监听鼠标“点击”、“移动”和“松开”三个事件,在鼠标移动时判断是否在“按下”的状态,若是则调用canvas功能进行绘图。
复制功能则靠canvas.toDataURL()这个函数来实现,这个函数可以将当前的图像转化为字符串,只要把这个字符串作为img标签的src属性即可完成复制。
结合之前用的websocket发送字符串的功能,很快可以想到”在线黑板”的实现逻辑。
教师端通过canvas进行绘制-->把canvas图像转化成字符串之后发送给websocket-->websocket将该字符串广播-->学生端收到字符串后,将字符串转换为图像并显示
一些细节处理
这里课堂的canvas图像都是1000px*1000px的,转化为字符串后的大小大约在200~300KB,而呈现流畅的绘制过程一般要做到30帧,即每秒发送30次图像,这肯定对网速肯定还是有一定负担的。同时在测试的时候发现如果发送频率过高又会造成卡顿,因此又需要对发送频率做一些限制。
mousemove事件频率每秒大概一百多次,所以我采用的方法是每10次mousemove进行一次广播
var cnt=0;
function mouseMoveAction(e){
if (isMouseDown){
x2=e.offsetX;
y2=e.offsetY;
drawLine(x1,y1,x2,y2);
x1=x2;
y1=y2;
if (cnt%10 == 0)
broadcast();
cnt++;
}
}
但是有时还是会出现卡顿的情况,有时不卡顿,但广播频率太低的话也看上去会很卡,所以就感觉挺难平衡的。(也可以做一个“广播”按钮,只在教师端需要时对图像做一次广播)
顺便一提mousemove事件的监听频率好像比较玄学,网上有说每移动1像素就会触发一次,但是我自己测下来如果鼠标移动比较慢,的确是1像素1次触发,但是鼠标移动的比较快的时候又触发的比较少,很多像素点会漏掉。(但是平均也在一百多次每秒的频率,每次都广播肯定吃不消)
弹幕功能
前置准备
还是先来个简易版本(文字会从右侧飘到左侧)
<style>
.can{
border: 2px black solid;
position: absolute;
top:100px;
left:0px;
right:0px;
margin:auto;
}
</style>
<script>
var canvas;
var context;
window.onload=function(){
canvas=document.getElementById('canvas');
context=canvas.getContext("2d");
context.font="30px 黑体";
draw();
}
var barrageList=[];
var barrage={
text:'弹幕测试1~~~~~~~~~~~~~',
speed:5,
x:1000,
y:100
}
barrageList.push(barrage);
var barrage={
text:'弹幕测试2~~~~~~~~~~~~~',
speed:5,
x:1000,
y:300
}
barrageList.push(barrage);
var barrage={
text:'弹幕测试3~~~~~~~~~~~~~',
speed:5,
x:1000,
y:500
}
barrageList.push(barrage);
var barrage={
text:'弹幕测试4~~~~~~~~~~~~~',
speed:5,
x:1000,
y:700
}
barrageList.push(barrage);
function draw(){
context.clearRect(0,0,1000,1000);
for (var i=0;i<barrageList.length;i++){
context.fillText(barrageList[i].text,barrageList[i].x,barrageList[i].y);
barrageList[i].x-=barrageList[i].speed;
if (barrageList[i].x<-300) {
barrageList.splice(i,1);
i--;
}
}
// setTimeout(function(){
// draw()
// },10);
requestAnimationFrame(draw);
}
</script>
<body>
<div class='center' id="container">
<canvas class='can' id="canvas" width="1000" height="1000"></canvas>
</div>
</body>
实现原理
用一个数组存储当前的弹幕信息,每条弹幕有四个属性
- 弹幕文本内容
- 弹幕漂浮速度
- 弹幕当前x坐标
- 弹幕y坐标
每一帧的绘制都会先清除当前的内容,遍历弹幕数组,对每个弹幕进行绘制,并根据速度修正其x坐标(弹幕向左漂浮)。若x坐标小于-300(即确保其已经漂浮至最左侧且不可见)则将其从数组中移除。
和之前的做法类似,控制弹幕的可见性就是用一个checkbox作为选项,然后用ng-show控制可见性。
细节处理
因为只需要通过websocket获取一次弹幕的文本内容,绘制过程不涉及websocket的信息传递,全部都是本地运算完成的,所以不存在对网络的负担。用requestAnimationFrame来递归draw()函数,这个的功能是在当前帧渲染完毕后进行下一帧,比setTimeout固定时间间隔进行递归相比,requestAnimationFrame性能更好,而且更流畅(或者说时间间隔更加智能吧)。
为了弹幕的效果可以给弹幕的y坐标和速度作随机化处理(通过Math.random()生成一个0-1的随机数)。
效果和代码
教师端可以在黑板上选择不同的颜色和笔触进行绘制,学生端同步显示画面。教师和学生都可以发送弹幕,也都可以关闭弹幕功能。
blackboard.html
<style>
.can{
border: 2px black solid;
left:0px;
right:0px;
margin:auto;
}
.showBoard{
border: 2px black solid;
left:0px;
right:0px;
height:1000px;
width: 1000px;
margin:auto;
}
.barrage_show{
pointer-events: none;
position:absolute;
top:0px;
}
</style>
<body>
<div class='center' id="container" ng-show="isTeacher">
<canvas class='can' id="canvas" width="1000" height="1000"></canvas>
<br>
<input type="color" id="color" onchange="showColor()">选择颜色
<input type="range" id="width" onchange="showRange()">选择笔触
<input type="checkbox" id="eraser" onchange="switchEraser()">橡皮擦
<button onclick="broadcast()">广播</button>
<button onclick="clearCanvas()">清空</button>
<div id='show'></div>
</div>
<div ng-show="!isTeacher">
<div class="showBoard" id='showBoard'></div>
</div>
<div class='barrage_show' ng-show='barrageIsShow'>
<canvas width="1000" height="1000" id="barrage"></div>
<input id="barrageText" type="text">
<button onclick="barrageSubmit()">发送弹幕</button>
<input type="checkbox" id="showBarrage" ng-click="switchBarrage()" checked="checked">开启弹幕
</div>
</body>
websocketServer
if (str.type == 'blackboard') {
let mes={};
mes.type='blackboard';
mes.data={
str:str.text
};
broadcast(JSON.stringify(mes));
}
if (str.type == 'barrage') {
let mes={};
mes.type='barrage';
mes.data={
str:str.text
};
broadcast(JSON.stringify(mes));
}
class.js后端
//处理websocket响应部分
if (type == 'blackboard') {
var show=document.getElementById('showBoard');
show.innerHTML="<img src='"+data.str+"'>";
}
if (type == 'barrage') {
var barrage={
text:data.str,
speed:Math.ceil(Math.random()*5)+2,
x:1100,
y:Math.ceil(Math.random()*900)+100
}
barrageList.push(barrage);
}
//处理canvas绘制
{
var color='black';
var width=5;
var x1,y1,x2,y2;
var isMouseDown=false;
var canvas;
var context;
var container;
var isEraser=false;
var barrage_ctx;
var cnt=0;
window.onload=function(){
canvas=document.getElementById('canvas');
context=canvas.getContext("2d");
context.fillStyle='WHITE';
context.fillRect(0,0,1000,1000);
container=document.getElementById('container');
container.onmousedown=mouseDownAction;
document.onmouseup=mouseUpAction;
canvas.onmousemove=mouseMoveAction;
barrage_ctx=document.getElementById('barrage').getContext('2d');
barrage_ctx.font="30px 黑体";
draw();
}
function mouseDownAction(e){
isMouseDown=true;
// console.log(e);
x1=e.offsetX;
y1=e.offsetY;
console.log(x1,y1);
}
function mouseUpAction(){
isMouseDown=false;
}
function mouseMoveAction(e){
if (isMouseDown){
x2=e.offsetX;
y2=e.offsetY;
drawLine(x1,y1,x2,y2);
x1=x2;
y1=y2;
if (cnt%10 == 0)
broadcast();
cnt++;
console.log(cnt);
}
}
function drawLine(x1,y1,x2,y2){
context.beginPath();
context.moveTo(x1,y1);
context.lineWidth=width;
context.strokeStyle=color;
if (isEraser) context.strokeStyle='white';
context.lineTo(x2,y2);
context.stroke();
}
function showRange(){
console.log(document.getElementById('width').value);
width=document.getElementById('width').value/10;
}
function showColor(){
console.log(document.getElementById('color').value);
color=document.getElementById('color').value;
}
function broadcast(){
console.log(canvas.toDataURL("image/png",0.92));
var data={
type:"blackboard",
text:canvas.toDataURL("image/png",0.92)
}
websocket.send(JSON.stringify(data));
}
function clearCanvas(){
context.fillStyle='WHITE';
context.fillRect(0,0,1000,1000);
broadcast();
// context.clear();
}
function switchEraser(){
isEraser=!isEraser;
}
function barrageSubmit(){
var data={
type:'barrage',
text:document.getElementById('barrageText').value
}
websocket.send(JSON.stringify(data));
}
var barrageList=[];
function draw(){
console.log('!!!');
barrage_ctx.clearRect(0,0,1000,1000);
for (var i=0;i<barrageList.length;i++){
barrage_ctx.fillText(barrageList[i].text,barrageList[i].x,barrageList[i].y);
barrageList[i].x-=barrageList[i].speed;
if (barrageList[i].x<-300) {
barrageList.splice(i,1);
i--;
}
}
requestAnimationFrame(draw);
}
}