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>

实现原理

用一个数组存储当前的弹幕信息,每条弹幕有四个属性

  1. 弹幕文本内容
  2. 弹幕漂浮速度
  3. 弹幕当前x坐标
  4. 弹幕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);
    }
}