You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

HTML Canvas鼠标移动时重复绘制形状问题求助

Canvas 形状拖拽绘制时重复堆叠问题

我的HTML Canvas在鼠标移动绘制矩形、圆形、三角形时,会在原有形状内部重复绘制相同形状,导致轮廓堆叠变粗,只需要显示单个形状轮廓。尝试过多种方案未解决,相关代码及问题截图如下:

Canvas鼠标移动时形状重复绘制堆叠

原代码

var colorLine = useRef('black')
var thick = useRef(1)
 
var selectedTool = useRef('brush')
var pressedMouse = useRef(false); 

useEffect(() => {

    var square = document.getElementById("drawPlace");
    square.height = window.innerHeight - 55;
    square.width = window.innerWidth - 65;
    var paper = square.getContext("2d");
    
    var x;
    var y;
    var snapshot;
    
    
    var key = {C: 67};


    const drawRect = (e, board) => {
       board.strokeRect(e.offsetX, e.offsetY, x-e.offsetX, y-e.offsetY)
   }

    const drawCircle = (e) => {
        paper.beginPath();
        let radius = Math.sqrt(Math.pow((x - e.offsetX), 2) + Math.pow((y - e.offsetY), 2));
        paper.arc(x, y, radius, 0, 2 * Math.PI);
        paper.stroke();
    }
    
    const drawTriangle = (e) => {
        paper.beginPath();
        paper.moveTo(x, y);
        paper.lineTo(e.offsetX, e.offsetY);
        paper.lineTo(x * 2 - e.offsetX, e.offsetY);
        paper.closePath();
        paper.stroke();
    }

    document.addEventListener("mousedown", startDrawing);
    document.addEventListener("mousemove", drawLine);
    document.addEventListener("mouseup", stopDrawing);
    document.addEventListener("keydown", clearCanvas);

    function startDrawing(eventvs01){
        pressedMouse.current = true;;
        x = eventvs01.offsetX;
        y = eventvs01.offsetY;
        paper.beginPath();
        //snapshot = paper.getImageData(0, 0, square.width, square.height);
    }

    function drawLine(eventvs02) {
        if (pressedMouse.current) {
            document.getElementById("drawPlace").style.cursor = `url(${dot})`;

            
            if(selectedTool.current === "brush") {
                var xM = eventvs02.offsetX;
                var yM = eventvs02.offsetY;
                drawing_line(colorLine.current, x, y, xM, yM, paper);
                x = xM;
                y = yM;

            }else if(selectedTool.current === "rectangle"){
                drawRect(eventvs02, paper);
            }else if(selectedTool.current === "circle"){
                drawCircle(eventvs02);
                
            }else {
                drawTriangle(eventvs02);
                
            }

        }
    }

    function stopDrawing(eventvs03) {
        pressedMouse.current = false;
        document.getElementById("drawPlace").style.cursor = "default";
    }

    function clearCanvas(whenPressKey) {
        if (whenPressKey.keyCode === key.C) {
            paper.clearRect(0, 0, square.width, square.height);
        }
    }

    function drawing_line(color, x_start, y_start, x_end, y_end, board){
        board.beginPath();
        board.strokeStyle = color;
        board.lineWidth = thick.current;
        board.moveTo(x_start,y_start);
        board.lineTo(x_end,y_end);
        board.stroke(); 
        board.closePath();
    }

    let handleCol = e => {
        [...[...document.querySelectorAll('.writing-set')][0].children].map((item, index ) => {

            if(index !== 6){

            
                item.addEventListener('click', (e) => {
                    console.log(e.currentTarget.dataset.col)

                    let color = e.currentTarget.dataset.col;
                    colorLine.current=color;
                })
            }else{
                item.addEventListener('change', (e) => {
                    thick.current = e.target.value;
                })
                
            }
        })
    }

    let handleShapes = e => {
        [...[...document.querySelectorAll('.writing-set')][2].children].map((item, index ) => {

            if(index !== 6){
                item.addEventListener('click', (e) => {
                    selectedTool.current = e.currentTarget.dataset.shape;
                    
                })
            }
        })
    }

    handleShapes();
    handleCol();

}, [colorLine])

解决方法

核心问题

鼠标移动时,每次mousemove事件都会在画布上新增一次形状绘制,之前的临时形状没有被清除,导致多层叠加。需要用画布快照实现"拖拽预览"效果:鼠标按下时保存当前画布状态,移动时先恢复快照再绘制临时形状,松开鼠标时再把最终形状绘制到画布上。

修改后的代码

var colorLine = useRef('black')
var thick = useRef(1)
 
var selectedTool = useRef('brush')
var pressedMouse = useRef(false); 

useEffect(() => {

    var square = document.getElementById("drawPlace");
    square.height = window.innerHeight - 55;
    square.width = window.innerWidth - 65;
    var paper = square.getContext("2d");
    
    var x;
    var y;
    var snapshot;
    
    var key = {C: 67};

    // 统一设置绘制样式
    paper.strokeStyle = colorLine.current;
    paper.lineWidth = thick.current;

    const drawRect = (e, board) => {
       const width = x - e.offsetX;
       const height = y - e.offsetY;
       board.strokeRect(e.offsetX, e.offsetY, width, height);
   }

    const drawCircle = (e) => {
        paper.beginPath();
        let radius = Math.sqrt(Math.pow((x - e.offsetX), 2) + Math.pow((y - e.offsetY), 2));
        paper.arc(x, y, radius, 0, 2 * Math.PI);
        paper.stroke();
    }
    
    const drawTriangle = (e) => {
        paper.beginPath();
        paper.moveTo(x, y);
        paper.lineTo(e.offsetX, e.offsetY);
        paper.lineTo(x * 2 - e.offsetX, e.offsetY);
        paper.closePath();
        paper.stroke();
    }

    // 新增:最终绘制形状的方法,鼠标松开时调用
    const drawFinalShape = (e) => {
        paper.strokeStyle = colorLine.current;
        paper.lineWidth = thick.current;
        switch(selectedTool.current) {
            case "rectangle":
                drawRect(e, paper);
                break;
            case "circle":
                drawCircle(e);
                break;
            case "triangle":
                drawTriangle(e);
                break;
        }
    }

    document.addEventListener("mousedown", startDrawing);
    document.addEventListener("mousemove", drawLine);
    document.addEventListener("mouseup", stopDrawing);
    document.addEventListener("keydown", clearCanvas);

    function startDrawing(eventvs01){
        pressedMouse.current = true;
        x = eventvs01.offsetX;
        y = eventvs01.offsetY;
        // 启用快照:保存鼠标按下时的画布状态
        snapshot = paper.getImageData(0, 0, square.width, square.height);
        // 更新绘制样式(防止颜色/粗细变化后不生效)
        paper.strokeStyle = colorLine.current;
        paper.lineWidth = thick.current;
    }

    function drawLine(eventvs02) {
        if (pressedMouse.current) {
            document.getElementById("drawPlace").style.cursor = `url(${dot})`;

            if(selectedTool.current === "brush") {
                var xM = eventvs02.offsetX;
                var yM = eventvs02.offsetY;
                drawing_line(colorLine.current, x, y, xM, yM, paper);
                x = xM;
                y = yM;

            } else {
                // 恢复快照,清除上一次的临时形状
                paper.putImageData(snapshot, 0, 0);
                // 绘制当前临时形状
                switch(selectedTool.current) {
                    case "rectangle":
                        drawRect(eventvs02, paper);
                        break;
                    case "circle":
                        drawCircle(eventvs02);
                        break;
                    case "triangle":
                        drawTriangle(eventvs02);
                        break;
                }
            }
        }
    }

    function stopDrawing(eventvs03) {
        if (pressedMouse.current && selectedTool.current !== "brush") {
            // 恢复快照后绘制最终形状
            paper.putImageData(snapshot, 0, 0);
            drawFinalShape(eventvs03);
        }
        pressedMouse.current = false;
        document.getElementById("drawPlace").style.cursor = "default";
    }

    function clearCanvas(whenPressKey) {
        if (whenPressKey.keyCode === key.C) {
            paper.clearRect(0, 0, square.width, square.height);
        }
    }

    function drawing_line(color, x_start, y_start, x_end, y_end, board){
        board.beginPath();
        board.strokeStyle = color;
        board.lineWidth = thick.current;
        board.moveTo(x_start,y_start);
        board.lineTo(x_end,y_end);
        board.stroke(); 
        board.closePath();
    }

    let handleCol = e => {
        [...[...document.querySelectorAll('.writing-set')][0].children].map((item, index ) => {
            if(index !== 6){
                item.addEventListener('click', (e) => {
                    colorLine.current = e.currentTarget.dataset.col;
                })
            }else{
                item.addEventListener('change', (e) => {
                    thick.current = e.target.value;
                })
            }
        })
    }

    let handleShapes = e => {
        [...[...document.querySelectorAll('.writing-set')][2].children].map((item, index ) => {
            if(index !== 6){
                item.addEventListener('click', (e) => {
                    selectedTool.current = e.currentTarget.dataset.shape;
                })
            }
        })
    }

    handleShapes();
    handleCol();

}, [colorLine])

修改说明

  1. 启用快照机制:startDrawing中恢复getImageData保存画布初始状态,drawLine中每次绘制前用putImageData恢复快照,清除之前的临时形状;
  2. 分离临时绘制与最终绘制:鼠标松开时,先恢复快照再绘制最终形状,确保画布上只有一个最终形状;
  3. 统一绘制样式:在startDrawing中更新画笔样式,避免颜色或粗细变化后预览形状样式不一致;
  4. 优化逻辑分支:用switch替代多if-else,让代码更清晰。

内容的提问来源于stack exchange,提问作者Chinedu Fabian

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 17:13:11