上一篇博客,我们分析了多人画板bug的产生原因,并且探索了一个解决的方法。现在让我们来实现它吧!
实现思路
场景再现:
我们再来回顾一下,用户绘制一条路径或者线条的步骤,一个moveTo方法,接着是一系列lineTo方法,并且注意在这个过程中画笔是一直保持在画布上的。你可以想象一下,你手握一只笔,在画布上从左往右画一条直线,就是那么简单的过程。那么现在,让我们加大一点复杂程度,此刻又来了一个人,也握住了笔。他想要画一条和你画的线平行的路径。现在,你们同时开始绘制路径了,你在上方画,他在下方画。对于画笔来说,就是上方画一笔,下方画一笔,笔在上下两条平行线之间来回切换,而且笔是一直在画布上的,我想大家都可以想象到那种混论的场景了。
解决方案:
我们给上面的两个人起一个代号:A、B。用户A先开始画,他向前移动了一点,然后他记住了他现在的位置。然后用户B开始画,他向前移动了一点,他也记住了他的位置。此刻又轮到了用户A,他先跳到他记住的那一点,然后开始绘制一点,更新当前点为他记住的点。。。这样每个人都记着自己上一步到达了哪里。但他再次开始绘制时,他先跳到他上次的地方,然后再开始绘制。
图例讲解
用户A、B绘制1、2、3这三个点的一种情况。
下面的1、2、3、4是步骤,假设A先画第1步,然后B画了第2步,接下来全部都是lineTo方法了,画笔只能在画布上移动,不能离开。然后A画到第3点,当时当前笔在B的第2点,所以是直接从下方拉到上方了。然后B画到他的第3点,笔是直接从上方拉到了下方。最终两条平行线绘制失败了。
用户A、B同时绘制,假设A先画第1步,然后B画第2步。然后A画下一步,此时需要先跳到他上一次绘制的点,然后开始画第4步(第3步用来跳点了),然后B想要绘制下一步,则需要先执行第5步,跳到上一次的位置,开始画他的第6步。
注意:B的第一步不会冲突,是因为第一步通常是按下鼠标,执行的是moveTo方法。
代码实现
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<style type="text/css">
* {
margin: 0;
padding: 0;
}
.rg{
float: left;
width: 400px;
height: 100px;
text-align: center;
border: 1px black solid;
margin-left:-1px ;
}
#cas{
width: 800px;
height: 600px;
border: #000000 1px solid;
}
p{
margin: 5px 0 5px 0;
}
</style>
</head>
<body>
<div id="seclect">
<div class="rg" id="secc">
<p>选择画笔颜色</p>
<input type="color" id="cl"/>
</div>
<div class="rg" id="secw">
<p>选择画笔大小: <span id="size">1px</span></p>
<input type="range" onchange="setLineWidth(this)" value="1" min="1" max="10"/>
</div>
</div>
<div id="cas">
<canvas id="cs" width="800" height="600"></canvas>
</div>
<script type="text/javascript">
var canvas = document.getElementById("cs");//获取画布
var context = canvas.getContext("2d");
const map = new Map(); // 用来记住每个人上一步的数据结构map
function setLineWidth(e) { // this 指向是就是该元素本身
console.log("你点击了画笔:", e);
console.log(e.value)
context.lineWidth = e.value;
document.getElementById("size").innerHTML = e.value + " px";
}
/* 用户绘制的动作,可以分解为如下操作:
1.按下鼠标
2.移动鼠标
3.松开鼠标
它们分别对应于鼠标的onmousedown、onmousemove和onmouseup事件。
并且上述操作必然是有想后顺序的,因为人的操作必然是几个操作
集合中的一种。所以我们需要来限定以下,过滤用户的无效操作,
只对按照上诉顺序的操作进行响应。
*/
let isDowned = false; // 是否按下鼠标,默认是false,如果为false,则不响应任何事件。
// 开始添加鼠标事件
canvas.onmousedown = function(e) {
let x = e.clientX - canvas.offsetLeft;
let y = e.clientY - canvas.offsetTop;
isDowned = true; // 设置isDowned为true,可以响应鼠标移动事件
console.log("当前鼠标点击的坐标为:(", x + ", " + y + ")");
context.strokeStyle = document.getElementById("cl").value; // 设置颜色,大小已经设置完毕了
context.beginPath(); // 开始一个新的路径
context.moveTo(x, y); // 移动画笔到鼠标的点击位置
// 多人协作的逻辑
let pos = {id: 0, type: 0, x: x, y: y, color: context.strokeStyle, size: context.lineWidth}
map.set(pos.id, pos) // 保存当前点为上一个点
client.send(JSON.stringify(pos))
}
canvas.onmousemove = function(e) {
if (!isDowned) {
return ;
}
let x = e.clientX - canvas.offsetLeft;
let y = e.clientY - canvas.offsetTop;
console.log("当前鼠标的坐标为:(", x + ", " + y + ")");
// 多人协作逻辑
let pos = {id: 0, type: 1, x: x, y: y, color: context.strokeStyle, size: context.lineWidth}
let prePoint = map.get(pos.id);
console.log(prePoint)
context.moveTo(prePoint.x, prePoint.y);
context.lineTo(x, y); // 移动画笔绘制线条
context.stroke();
map.set(pos.id, pos) // 保存当前点为上一个点
client.send(JSON.stringify(pos))
}
canvas.onmouseup = function(e) {
isDowned = false;
}
/*
在按下鼠标移动的过程中,如果移出了画布,则无法触发鼠标松开事件,即onmouseup。
所以需要在鼠标移出画布时,设置isDowned为false。
*/
canvas.onmouseout = function(e) {
isDowned = false;
}
</script>
<script>
function link () {
client = new WebSocket("ws://crazydragon.top:10000/ws/wedraw"); //连接服务器
client.onopen = function(e){
alert('连接了');
};
client.onmessage = function (e) {
let data = e.data
let pos = JSON.parse(data)
console.log("接受到的消息:" + data)
context.strokeStyle = pos.color // 设置颜色
context.lineWidth = pos.size // 设置线宽
if (pos.type === 0) { // 如果该点是移动画笔,则移动画笔
context.beginPath() // 开始一个新的路径
context.moveTo(pos.x, pos.y)
context.stroke(); // 绘制点,这行代码似乎没有作用
map.set(pos.id, pos) // 记住当前点的位置
} else if (pos.type === 1) { // 如果该点是画线,就画线
// 对于画线,不再是直接画线,而是要区分它是谁的那一笔
let mpos = map.get(pos.id)
// 开始一个新路径
context.beginPath();
context.lineWidth = mpos.size;
context.strokeStyle = mpos.color;
// 然后移动到它上一点的位置
context.moveTo(mpos.x, mpos.y)
console.log("正在画线")
context.lineTo(pos.x, pos.y)
// 记录当前点的位置,覆盖原来的记录,这里默认所有的点都是moveTo了,不需要修改type了
map.set(pos.id, pos)
context.stroke(); // 绘制点
} else {
console.log("不存在的情况,直接返回")
return
}
}
client.onclose = function(e){
alert("已经与服务器断开连接\r\n当前连接状态:" + this.readyState);
};
client.onerror = function(e){
alert("WebSocket异常!");
};
}
function sendMsg(position){
client.send(position);
}
link () // 直接建立websocket连接
</script>
</body>
</html>
使用一个map结构来存储每个用户绘制的当前点,作为下一次绘制的上一点。每次绘制时,则向跳到他的上一点,再进行绘制。
测试结果
总结
可以发现,现在两个用户(或者多个用户)是可以进行同时绘制了。 但是绘制的线条的演示和大小会进行干扰,同时这种方式绘制出来的线条或者路径,感觉丢失了一些细节,虽然整体的结构还是保持的。这是因为原来那种绘制方法被打破了:
原来的绘制方式是,一个moveTo,然后接着执行一系列的lineTo方法。
当时为了配合多个用户进行绘制,现在的绘制方式是,一个moveTo,一个lineTo,再接着moveTo,然后lineTo…,也就是说原来是直接绘制一条线段,当时现在变成了一条一条线段的绘制了。当时这个问题,在后面会有解决方式,我们放在下一篇进行讲解。