我正在开发一个使用HTML5画布的协作白板.我想用鼠标绘制形状,为此,我要清除整个画布,重新绘制之前在画布上的元素,然后在上面添加新的形状.虽然这很管用,但它会产生如下口吃:

The stutter:

如果我不清除画布,我会得到这样的结果:

Multiple shapes drawn when I don't clear:

我正在使用Socket.IO在客户端之间发送数据.在画布的鼠标释放事件侦听器上,我发出如下数据:

canvas.addEventListener('mouseup', function () {
                canvas.removeEventListener('mousemove', onPaint, false);
                canvas.isDrawing = false;
                let canvasImg = canvas.toDataURL('image/png')
                socket.emit('canvas-data', canvasImg)
            }, false);

然后我有一个react 挂钩来接收套接字事件并设置白板图像.然后将其绘制到画布上,如下所示:

useEffect(() => {
        if (open) {
            let image = new Image();
            let canvas = canvasRef.current;
            canvas.whiteboardImage = whiteboardImage
            let ctx = canvas.getContext('2d');
            image.onload = () => {
                ctx.drawImage(image, 0, 0)
            }
            image.src = whiteboardImage
        }
    }, [whiteboardImage, open])

在绘制形状时,我在鼠标移动事件侦听器中如下所示:

if (canvas.toolState === 'line') {
                    if (canvas.isDrawing) {
                        // clear and get old whiteboard image
                        ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
                        let image = new Image();
                        image.onload = () => {
                            ctx.drawImage(image, 0, 0)
                        }
                        image.src = canvas.whiteboardImage

                        // draw new line
                        ctx.beginPath();
                        ctx.moveTo(mouse.x, mouse.y);
                        ctx.lineTo(shape_start.x, shape_start.y);
                        ctx.stroke();
                    }
                } // ... all the other shapes

这个问题发生在所有形状上,但不是铅笔工具,因为当我用铅笔作画时,我不必清理画布.

据我所知,只清除一次是不可能的.即使我将绘制的所有内容存储为对象及其路径,我仍然必须清除画布并重新绘制,这仍然会导致卡顿.

有什么我能做的吗?我如何让它变得顺畅?

A Minimal, Reproducable Example

const Whiteboard = ({ open }) => {
    const canvasRef = useRef(null);
    const [whiteboardImage, setWhiteboardImage] = useState(null)

    // socket event to receive whiteboard data from other clients
    useEffect(() => {
        socket.on('canvas-data', (data) => {
                let image = new Image();
                let canvas = canvasRef.current;
                let ctx = canvas.getContext('2d');
                image.onload = () => {
                    ctx.drawImage(image, 0, 0)
                }
                image.src = data
                setWhiteboardImage(data)
            })
    }, [])

    // sets up mouselisteners if the canvas is open
    useEffect(() => {
        if (open) {
            drawOnCanvas()
        }
    }, [open, canvasRef])

    const drawOnCanvas = () => {
        let canvas = canvasRef.current;

        if (canvas) {
            let ctx = canvas.getContext('2d');
            let rect = canvas.getBoundingClientRect();

            let sketch = document.querySelector('#sketch');
            let sketch_style = getComputedStyle(sketch);
            canvas.width = parseInt(sketch_style.getPropertyValue('width'));
            canvas.height = parseInt(sketch_style.getPropertyValue('height'));

            // initializing objects
            let mouse = { x: 0, y: 0 };
            let last_mouse = { x: 0, y: 0 };
            let shape_start = { x: 0, y: 0 };

            /* Mouse Capturing Work */
            canvas.addEventListener('mousemove', function (e) {
                mouse.x = e.pageX - rect.x;
                mouse.y = e.pageY - rect.y;
            }, false);


            /* Drawing on Paint App */
            ctx.lineWidth = 5;
            ctx.lineJoin = 'round';
            ctx.lineCap = 'round';
            ctx.strokeStyle = 'black';

            canvas.addEventListener('mousedown', function (e) {
                shape_start = { x: mouse.x, y: mouse.y }
                canvas.isDrawing = true;
                canvas.addEventListener('mousemove', onPaint, false);
            }, false);

            canvas.addEventListener('mouseup', function () {
                canvas.removeEventListener('mousemove', onPaint, false);
                canvas.isDrawing = false;
                let canvasImg = canvas.toDataURL('image/png')
                socket.emit('canvas-data', canvasImg)
            }, false);

            // draws straight line with mouse
            let onPaint = function () {
                // clear and get old whiteboard image
                ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
                if (whiteboardImage) {
                    let image = new Image();
                    image.onload = () => {
                        ctx.drawImage(image, 0, 0)
                    }
                    image.src = whiteboardImage
                }
                // draw new line
                ctx.beginPath();
                ctx.moveTo(mouse.x, mouse.y);
                ctx.lineTo(shape_start.x, shape_start.y);
                ctx.stroke();
            }
        }

        return (
            <div>
                <canvas ref={canvasRef} />
            </div>
        )
    }

我试着把它弄得尽可能少.在本例中,您只能绘制线条,没有像gif中那样的画笔工具.卡顿来自于我在onPaint函数中绘制形状的方式.清算和重新提款是问题所在.

推荐答案

您加载白板图像每次鼠标移动调用-它会导致闪烁的图像.您应该平稳地加载它,并且只在画布图形中使用加载的图像(不要在那里加载它).

const [image, setImage] = useState(null);
// next line not required anymore
// const [whiteboardImage, setWhiteboardImage] = useState(null)


useEffect(() => {
    socket.on('canvas-data', (data) => {
        let image = new Image();
        let canvas = canvasRef.current;
        let ctx = canvas.getContext('2d');
        image.onload = () => {
             setImage(image);
        }
        image.src = data;
    });
}, []);

useEffect(() => {
    if (open && canvasRef.current) {
        drawOnCanvas()
    }
}, [open, canvasRef, image])

const drawOnCanvas = () => {
    let canvas = canvasRef.current;
    let ctx = canvas.getContext('2d');
    let rect = canvas.getBoundingClientRect();

    ...

    // draws straight line with mouse
    let onPaint = function () {
        // clear and get old whiteboard image
        ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
        if (image) {
            ctx.drawImage(image, 0, 0);
        }
        // draw new line
        ctx.beginPath();
        ctx.moveTo(mouse.x, mouse.y);
        ctx.lineTo(shape_start.x, shape_start.y);
        ctx.stroke();
    }

    onPaint();
}

但正如Kaiido建议的那样,使用点和参数而不是图像数据要好得多.它将是这样的:

const Whiteboard = ({ open }) => {
    const canvasRef = useRef(null);
    const [elements, setElements] = useState(null)

    // socket event to receive whiteboard data from other clients
    useEffect(() => {
        socket.on('canvas-data', (data) => {
            setElements(data);
            // here data should be kind of list of canvas elements
            // it could be something like:
            // [
            //   {type: "line", start: {x: 0, y: 0}, end: {x: 40, y: 10}},
            //   {type: "circle", center: {x: 20, y: 20}, r: 7},
            //   {type: "polyline", points: [{x: 0, y: 0}, {x: 2, y: 2}, {x: 5, y, 3}, ...]},
            // ]
        })
    }, [])

    // sets up mouselisteners if the canvas is open
    useEffect(() => {
        if (open && canvasRef.current) {
            drawOnCanvas()
        }
    }, [open, canvasRef, elements])

    const drawElement = (ctx, elem) => {
        switch (elem.type){
            case 'line':
                ctx.beginPath();
                ctx.moveTo(elem.start.x, elem.start.y);
                ctx.lineTo(elem.end.x, elem.end.y);
                ctx.stroke();
            case 'polyline':
                // ...
        }
    };

    const drawOnCanvas = () => {
        let canvas = canvasRef.current;

        if (canvas) {
            let ctx = canvas.getContext('2d');
            let rect = canvas.getBoundingClientRect();

            let sketch = document.querySelector('#sketch');
            let sketch_style = getComputedStyle(sketch);
            canvas.width = parseInt(sketch_style.getPropertyValue('width'));
            canvas.height = parseInt(sketch_style.getPropertyValue('height'));

            /* Mouse Capturing Work */
            const getPos = (e) => {
                return {x: e.pageX - rect.x, y: e.pageY - rect.y};
            };


            /* Drawing on Paint App */
            ctx.lineWidth = 5;
            ctx.lineJoin = 'round';
            ctx.lineCap = 'round';
            ctx.strokeStyle = 'black';

            canvas.addEventListener('mousedown', function (e) {
                canvas.currentElement = {type: "line", start: getPos(e), end: getPos(e)};
                canvas.addEventListener('mousemove', onPaint, false);
            }, false);

            canvas.addEventListener('mouseup', function () {
                canvas.removeEventListener('mousemove', onPaint, false);
                elements.push(canvas.currentElement);
                socket.emit('canvas-data', elements);
                canvas.currentElement = null;
                setElements(elements);
            }, false);

            // draws straight line with mouse
            let onPaint = function (e) {
                if (e)
                    canvas.currentElement.end = getPos(e);
                // clear and get old whiteboard image
                ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
                elements.forEach(el => drawElement(ctx, el));
                if (e)
                    drawElement(ctx, canvas.currentElement);
            }

            onPaint();
        }
    };

    return (
        <div>
            <canvas ref={canvasRef} />
        </div>
    );
}

您可以看到,在本例中,我们只使用对象,我们需要为每种类型的元素(直线、圆等)实现绘制方法,然后在鼠标事件侦听器上,我们只需更改CurrentElement对象.

这种方法要好得多,因为它不依赖于画布大小.

使用你的方法,如果有人在手机屏幕(小屏幕)的右侧画圆圈,其他屏幕较宽的人将看到裁剪后的图像.在面向对象的方法中,这个问题被消除了.

希望这能有所帮助.

Reactjs相关问答推荐

阻止在ShadCN和React中的FormLabel中添加 colored颜色

无法使用#13;或br将新行设置为文本区域'"&"<>

使用REACT-RUTER-V6和ZUSTANDreact 中的身份验证

react ';S使用状态不设置新状态

FireBase Reaction Native-在呈现视图之前等待响应

子路径上未定义useMatches句柄

为什么我的react 简单计时器项目不起作用?

Github 操作失败:发布 NPM 包

滚动视图样式高度没有任何区别

面临 React 模式中点击外部条件的问题

将 useRef 与 useImperativeHandle 一起使用时,React 不会重新渲染

Syncfusion:带有 TabItemsDirective 的选项卡组件不显示任何内容

当`useSelector`回调中的属性值更新时,将会使用更新后的值吗?

AWS 全栈应用程序部署教程构建失败

理解 React 中的 PrevState

npm init vite@latest 和 npm init vite 有什么区别?

axios post方法中的请求失败,状态码为500错误

试图将页面限制为仅登录但有条件的用户似乎永远不会运行

由于另一个子组件,如何在react 中渲染另一个子组件

React Router 6,一次处理多个搜索参数