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

JavaScript Canvas带背景图的橡皮擦工具实现技术求助

基于Canvas的遮罩绘图工具橡皮擦实现方案

问题分析

你开发的工具需求是:底层为动态背景图,上层覆盖黑色遮罩,用户用白色线条绘制(相当于揭开遮罩露出背景),但直接修改strokeStyle无法实现真正的橡皮擦效果——黑色会覆盖白色线条,而非擦除遮罩/线条露出背景。

解决方案

核心思路是利用Canvas的**全局合成模式(globalCompositeOperation)**实现擦除:

  • 正常绘制用默认的source-over模式,白色线条覆盖在黑色遮罩上;
  • 橡皮擦模式切换为destination-out模式,此时绘制的内容会清除画布对应位置的像素(即擦除遮罩或线条,露出底层背景)。

同时需要完善绘图状态记录,确保重绘时能正确还原所有操作。

具体修改步骤

  1. 修复图层顺序:先绘制背景图,再叠加黑色遮罩,避免背景被遮罩覆盖;
  2. 添加模式记录:新增数组记录每一笔操作是绘制还是擦除,用于重绘还原;
  3. 修改橡皮擦切换逻辑:通过切换合成模式实现擦除效果;
  4. 完善重绘函数:根据记录的状态重新绘制所有操作,保证画布内容正确还原。

修改后的完整代码

<canvas id="sheet" width="800" height="600"></canvas>
<input type="range" id="strokeRange" min="1" max="20" value="5">
<span id="value">5</span>
<label><input type="checkbox" id="eraserCheckbox"> 橡皮擦</label>

<script>
    "use strict";
    var canvas = document.getElementById('sheet');
    var context = canvas.getContext("2d");

    let strokeValue = 5; // 默认画笔粗细
    let currentMode = 'draw'; // 当前模式:draw(绘制)/eraser(擦除)

    // 画笔粗细调节
    document.getElementById("strokeRange").addEventListener("input", function (event) {
        strokeValue = event.target.value;
        document.getElementById("value").innerHTML = strokeValue;
        context.lineWidth = strokeValue;
    });

    // 橡皮擦切换逻辑
    document.getElementById("eraserCheckbox").addEventListener('change', (event) => {
        if (event.currentTarget.checked) {
            currentMode = 'eraser';
            context.globalCompositeOperation = 'destination-out';
            context.strokeStyle = "#fff"; // 任意不透明颜色均可,合成模式决定擦除效果
        } else {
            currentMode = 'draw';
            context.globalCompositeOperation = 'source-over';
            context.strokeStyle = "#fff";
        }
    });

    // 加载背景图
    var background = new Image();
    background.src = "https://www.brettonwoodsproject.org/wp-content/uploads/2017/10/512px-Rembrandt_Christ_in_the_Storm_on_the_Lake_of_Galilee-1.jpg";

    background.onload = function(){
        // 先绘制背景,再叠加黑色遮罩
        context.drawImage(background,0,0);   
        context.fillStyle = "black";
        context.fillRect(0, 0, canvas.width, canvas.height);
    };

    // 初始化绘图状态
    context.strokeStyle = "#fff";
    context.lineJoin = "round";
    context.lineWidth = strokeValue;

    // 绘图记录数组
    var clickX = [];
    var clickY = [];
    var clickDrag = [];
    var clickMode = []; // 记录每一笔的操作模式
    var paint;

    /**
     * 添加点击记录
     * @param {number} x 横坐标
     * @param {number} y 纵坐标
     * @param {boolean} dragging 是否拖拽中
     * @param {string} mode 操作模式
     */
    function addClick(x, y, dragging, mode) {
        clickX.push(x);
        clickY.push(y);
        clickDrag.push(dragging);
        clickMode.push(mode);
    }

    /**
     * 重绘整个画布
     */
    function redraw() {
        // 清空画布
        context.clearRect(0, 0, canvas.width, canvas.height);
        // 重绘背景图
        context.drawImage(background,0,0); 
        // 重绘黑色遮罩
        context.fillStyle = "black";
        context.fillRect(0, 0, canvas.width, canvas.height);

        // 重置绘图基础样式
        context.lineJoin = "round";
        context.lineWidth = strokeValue;

        // 遍历所有绘图记录逐一重绘
        for (var i = 0; i < clickX.length; i++) {
            // 根据记录的模式切换合成模式
            if (clickMode[i] === 'eraser') {
                context.globalCompositeOperation = 'destination-out';
            } else {
                context.globalCompositeOperation = 'source-over';
                context.strokeStyle = "#fff";
            }

            if (!clickDrag[i]) {
                if (i === 0) {
                    context.beginPath();
                    context.moveTo(clickX[i], clickY[i]);
                    context.stroke();
                } else {
                    context.closePath();
                    context.beginPath();
                    context.moveTo(clickX[i], clickY[i]);
                    context.stroke();
                }
            } else {
                context.lineTo(clickX[i], clickY[i]);
                context.stroke();
            }
        }

        // 恢复当前操作模式状态
        if (currentMode === 'eraser') {
            context.globalCompositeOperation = 'destination-out';
        } else {
            context.globalCompositeOperation = 'source-over';
            context.strokeStyle = "#fff";
        }
    }

    /**
     * 绘制新添加的点
     */
    function drawNew() {
        var i = clickX.length - 1;
        if (!clickDrag[i]) {
            if (i === 0) {
                context.beginPath();
                context.moveTo(clickX[i], clickY[i]);
                context.stroke();
            } else {
                context.closePath();
                context.beginPath();
                context.moveTo(clickX[i], clickY[i]);
                context.stroke();
            }
        } else {
            context.lineTo(clickX[i], clickY[i]);
            context.stroke();
        }
    }

    function mouseDownEventHandler(e) {
        paint = true;
        var x = e.pageX - canvas.offsetLeft;
        var y = e.pageY - canvas.offsetTop;
        if (paint) {
            addClick(x, y, false, currentMode);
            drawNew();
        }
    }

    function touchstartEventHandler(e) {
        paint = true;
        if (paint) {
            addClick(e.touches[0].pageX - canvas.offsetLeft, e.touches[0].pageY - canvas.offsetTop, false, currentMode);
            drawNew();
        }
    }

    function mouseUpEventHandler(e) {
        context.closePath();
        paint = false;
    }

    function mouseMoveEventHandler(e) {
        var x = e.pageX - canvas.offsetLeft;
        var y = e.pageY - canvas.offsetTop;
        if (paint) {
            addClick(x, y, true, currentMode);
            drawNew();
        }
    }

    function touchMoveEventHandler(e) {
        if (paint) {
            addClick(e.touches[0].pageX - canvas.offsetLeft, e.touches[0].pageY - canvas.offsetTop, true, currentMode);
            drawNew();
        }
    }

    function setUpHandler(isMouseandNotTouch, detectEvent) {
        removeRaceHandlers();
        if (isMouseandNotTouch) {
            canvas.addEventListener('mouseup', mouseUpEventHandler);
            canvas.addEventListener('mousemove', mouseMoveEventHandler);
            canvas.addEventListener('mousedown', mouseDownEventHandler);
            mouseDownEventHandler(detectEvent);
        } else {
            canvas.addEventListener('touchstart', touchstartEventHandler);
            canvas.addEventListener('touchmove', touchMoveEventHandler);
            canvas.addEventListener('touchend', mouseUpEventHandler);
            touchstartEventHandler(detectEvent);
        }
    }

    function mouseWins(e) {
        setUpHandler(true, e);
    }

    function touchWins(e) {
        setUpHandler(false, e);
    }

    function removeRaceHandlers() {
        canvas.removeEventListener('mousedown', mouseWins);
        canvas.removeEventListener('touchstart', touchWins);
    }

    canvas.addEventListener('mousedown', mouseWins);
    canvas.addEventListener('touchstart', touchWins);
</script>

关键说明

  • destination-out合成模式:该模式下绘制的内容会完全清除画布对应位置的像素,完美适配擦除遮罩露出背景的需求;
  • 状态记录:clickMode数组记录每一笔操作模式,确保重绘时能准确还原所有绘制和擦除效果;
  • 图层顺序:背景图加载完成后再绘制黑色遮罩,保证背景在最底层,遮罩层在上方。

内容的提问来源于stack exchange,提问作者M. Özdemir

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 08:19:54