socket+fabricjs 实现画板同步

2,100

「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

相关库:socket.io、fabricjs

思路

A通过socket链接传输canvas数据,express做转发,B监听socket得到数据并渲染。

实施

1.首先把配置弄好,装好socket.io和express,这里的fabricjs由于下载太慢了我用的文件。

2.配置server定义好命令paint具体的用type来区分 1:画笔、2:选择、3:橡皮擦

const express=require('express');
const app=express();

app.use(express.static(__dirname));

const server=require('http').createServer(app)
const io=require('socket.io')(server)

//监听客户端的链接事件

io.on('connection',socket=>{
    socket.on('paint',function(data){
        io.emit('paint',data)
    })
});
server.listen(3000)
console.log('http://127.0.0.1:3000')

这里打印一下地址方便访问

3.先用prompt+时间戳来区分user,用fabricjs 生成画布

let canvas = new fabric.Canvas("canvas", {
    selection: false,//是否采用选区
    skipTargetFind: true,//跳过目标查找
    enableRetinaScaling: false,//是否启动缩放
});

4.在生成的canvas上再new一个画笔let brush = new fabric.PencilBrush(canvas);

5.给canvas注册事件监听到鼠标mousedown、mousemove、mouseup的同时调用画笔对应的方法并发送socket命令,由于同步操作需要一个唯一的值,所以在mousedown的时候要生成一个自定义的id用来区分画布上的对象(canvas.toJSON()时需要在括号里带上这个自定义属性不然序列化后的数据会没有自定义属性), scoket监听对应的命令时需要做对应的操作(mousedown、mousemove、mouseup)

canvas.on("mouse:down", (data) => {
    if (drawMode) {
        curDrawObjectId = new Date().getTime();
        let pointer = data.pointer;
        socket.emit("paint", {
            type: 1,
            login_name: cache_name,
            data: {
                id: curDrawObjectId,
                type: "mouseDown",
                point: data,
                bruchColor: brush.color,
            },
        });
        drawing = true;
        brush.onMouseDown(pointer, data);
    }
});
canvas.on("mouse:move", (data) => {
    if (drawMode && drawing) {
        let pointer = data.pointer;
        socket.emit("paint", {
            type: 1,
            login_name: cache_name,
            data: {
                id: curDrawObjectId,
                type: "mouseMove",
                point: data,
                bruchColor: brush.color,
            },
        });
        brush.onMouseMove(pointer, data);
    }
});
canvas.on("mouse:up", (data) => {
    if (drawMode && drawing) {
        let pointer = data.pointer;
        brush.onMouseUp(data);
        let len = canvas.getObjects().length;
        canvas.item(len - 1).id = curDrawObjectId;
        socket.emit("paint", {
            type: 1,
            login_name: cache_name,
            data: {
                id: curDrawObjectId,
                type: "mouseUp",
                point: data,
                bruchColor: brush.color,
            },
        });
        drawing = false;
    }
});

6.画布上的对象操作时也需要判断自定义的id,对象移动时要发送对象的x、y坐标

canvas.on("object:moving", (e) => {
    socket.emit("paint", {
        type: 2,
        login_name: cache_name,
        data: {
            type: "move",
            data: {
                id: e.target.id,
                x: e.target.left,
                y: e.target.top
            },
        },
    });
});
if (data.data.type == "move") {
    let d = data.data.data;
    if (!d.id) return;
    canvas.getObjects().forEach((v) => {
        if (v.id == d.id) {
            v.set({
                left: d.x,
                top: d.y
            });
            canvas.renderAll();
        }
    });
} 

旋转和缩放需要角度和比例

canvas.on("object:scaling", (e) => {
    e.target.set({
        flipX: false,
        flipY: false
    });
    socket.emit("paint", {
        type: 2,
        login_name: cache_name,
        data: {
            type: "scale",
            data: {
                id: e.target.id,
                scaleX: e.target.scaleX,
                scaleY: e.target.scaleY,
            },
        },
    });
});
canvas.on("object:rotating", (e) => {
    socket.emit("paint", {
        type: 2,
        login_name: cache_name,
        data: {
            type: "rotate",
            data: {
                id: e.target.id,
                angle: e.target.angle,
            },
        },
    });
});

擦除使用的是点击擦除所以用的是创建选区的事件

canvas.on("selection:created", (e) => {
    if (remove == true) {
        if (e.target.type !== "image") {
            socket.emit("paint", {
                type: 3,
                login_name: cache_name,
                data: {
                    id: e.target.id
                },
            });
            canvas.remove(e.target);
        }
    }
});

8.需要注意的就是需要的属性方法需要去官方文档上查找或者打印canvas画布上的对象,发送命令和监听命令时不要造成死循环了,对于操作比较影响性能的需要使用canvas.renderAll()重绘,不然会很卡顿。是用了三个按钮区分画笔选择和擦除功能的,画笔颜色选择后可以同步。

image.png

源码:github.com/F-howk/sock…

结语

有一个小问题是A画好后B修改一下然后A有可能会删除不了不是必现的找不到问题在哪儿。。。 demo还可以优化,其实最好是可以封装成类方便管理代码也比较清晰,有不足之处希望大佬能多指点指点。