JavaScript Canvas带背景图的橡皮擦工具实现技术求助
基于Canvas的遮罩绘图工具橡皮擦实现方案
问题分析
你开发的工具需求是:底层为动态背景图,上层覆盖黑色遮罩,用户用白色线条绘制(相当于揭开遮罩露出背景),但直接修改strokeStyle无法实现真正的橡皮擦效果——黑色会覆盖白色线条,而非擦除遮罩/线条露出背景。
解决方案
核心思路是利用Canvas的**全局合成模式(globalCompositeOperation)**实现擦除:
- 正常绘制用默认的
source-over模式,白色线条覆盖在黑色遮罩上; - 橡皮擦模式切换为
destination-out模式,此时绘制的内容会清除画布对应位置的像素(即擦除遮罩或线条,露出底层背景)。
同时需要完善绘图状态记录,确保重绘时能正确还原所有操作。
具体修改步骤
- 修复图层顺序:先绘制背景图,再叠加黑色遮罩,避免背景被遮罩覆盖;
- 添加模式记录:新增数组记录每一笔操作是绘制还是擦除,用于重绘还原;
- 修改橡皮擦切换逻辑:通过切换合成模式实现擦除效果;
- 完善重绘函数:根据记录的状态重新绘制所有操作,保证画布内容正确还原。
修改后的完整代码
<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
相关产品推荐
相关产品推荐

