如何在HTML Canvas透明背景下实现像素涂抹效果?
透明背景下Canvas涂抹效果异常的解决方法
问题说明
我想要在HTML Canvas上实现Stack Overflow用户Blindman67的涂抹效果,但需要使用透明背景——也就是注释掉原代码中填充白色背景的两行:
//background.ctx.fillStyle = "white"; //background.ctx.fillRect(0,0,w,h);
注释后涂抹的像素出现奇怪的深色边缘并不断扩散,无法实现透明像素涂抹到可见像素时的干净效果。
问题代码
<canvas id="canvas"></canvas> <style> canvas { position: absolute; top: 0px; left: 0px; } </style> <script> "use strict"; var drawon_ctx = canvas.getContext("2d"); //is our drawon var tmp_canvas = createCanvas(canvas.width, canvas.height); //is our tmp var tmp_ctx = tmp_canvas.ctx; // var brushSize = 64; var bs = 64; var bsh = bs / 2; var smudgeAmount = 0.25; // values from 0 none to 1 full // helpers var doFor = function doFor(count, cb) { var i = 0; while (i < count && cb(i++) !== true) { ; } }; // the ; after while loop is important don't remove var randI = function randI(min) { var max = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : min + (min = 0); return Math.random() * (max - min) + min | 0; }; // simple mouse var mouse = { x: 0, y: 0, button: false }; function mouseEvents(e) { mouse.x = e.pageX; mouse.y = e.pageY; mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button; } ["down", "up", "move"].forEach(function (name) { return document.addEventListener("mouse" + name, mouseEvents); }); // brush gradient for feather var grad = drawon_ctx.createRadialGradient(bsh, bsh, 0, bsh, bsh, bsh); //center coords/ bsh is half of bs grad.addColorStop(0, "black"); grad.addColorStop(1, "rgba(0,0,0,0)"); var v_brush = createCanvas(bs); // our v_brush // creates an offscreen canvas function createCanvas(w) { var h = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : w; var c = document.createElement("canvas"); c.width = w; c.height = h; c.ctx = c.getContext("2d"); return c; } // get the brush from source ctx at x,y function brushFrom(tmp_ctx, x, y) { v_brush.ctx.globalCompositeOperation = "source-over"; v_brush.ctx.globalAlpha = 1; v_brush.ctx.drawImage(tmp_canvas, -(x - bsh), -(y - bsh)); // v_brush.ctx.drawImage(tmp_ctx.canvas, -(x - bsh), -(y - bsh)); v_brush.ctx.globalCompositeOperation = "destination-in"; v_brush.ctx.globalAlpha = 1; v_brush.ctx.fillStyle = grad; v_brush.ctx.fillRect(0, 0, bs, bs); } // short cut vars var w = canvas.width; var h = canvas.height; var cw = w / 2; // center var ch = h / 2; var globalTime; var lastX; var lastY; // update tmp_canvas is size changed function createBackground() { tmp_canvas.width = w; tmp_canvas.height = h; // tmp_ctx.fillStyle = "white"; // tmp_ctx.fillRect(0, 0, w, h); doFor(64, function () { tmp_ctx.fillStyle = "rgb(".concat(randI(255), ",").concat(randI(255), ",").concat(randI(255)); tmp_ctx.fillRect(randI(w), randI(h), randI(10, 100), randI(10, 100)); }); } // main update function function update(timer) { globalTime = timer; drawon_ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform drawon_ctx.globalAlpha = 1; // reset alpha if (w !== innerWidth || h !== innerHeight) { cw = (w = canvas.width = innerWidth) / 2; ch = (h = canvas.height = innerHeight) / 2; createBackground(); } else { drawon_ctx.clearRect(0, 0, w, h); } drawon_ctx.drawImage(tmp_canvas, 0, 0); // if mouse down then do the smudge for all pixels between last mouse and mouse now if (mouse.button) { v_brush.ctx.globalAlpha = smudgeAmount; var dx = mouse.x - lastX; var dy = mouse.y - lastY; var dist = Math.sqrt(dx * dx + dy * dy); for (var i = 0; i < dist; i += 1) { var ni = i / dist; brushFrom(tmp_ctx, lastX + dx * ni, lastY + dy * ni); ni = (i + 1) / dist; tmp_ctx.drawImage(v_brush, lastX + dx * ni - bsh, lastY + dy * ni - bsh); } } else { v_brush.ctx.clearRect(0, 0, bs, bs); /// clear brush if not used } lastX = mouse.x; lastY = mouse.y; requestAnimationFrame(update); } requestAnimationFrame(update); </script>
解决方案
问题根源
透明背景下,Canvas默认像素为黑色透明(rgba(0,0,0,0)),原代码涂抹时会将这些透明黑色像素混合到可见像素中,导致边缘出现深色扩散。需要修改采样和绘制逻辑,只处理不透明的可见像素。
修复后的完整代码
<canvas id="canvas"></canvas> <style> canvas { position: absolute; top: 0px; left: 0px; } </style> <script> "use strict"; var drawon_ctx = canvas.getContext("2d"); var tmp_canvas = createCanvas(canvas.width, canvas.height); var tmp_ctx = tmp_canvas.ctx; var bs = 64; var bsh = bs / 2; var smudgeAmount = 0.25; // 辅助函数 var doFor = function doFor(count, cb) { var i = 0; while (i < count && cb(i++) !== true) {} }; var randI = function randI(min, max) { if (max === undefined) { max = min; min = 0; } return Math.random() * (max - min) + min | 0; }; // 鼠标事件处理 var mouse = { x: 0, y: 0, button: false }; function mouseEvents(e) { mouse.x = e.pageX; mouse.y = e.pageY; mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button; } ["down", "up", "move"].forEach(function (name) { return document.addEventListener("mouse" + name, mouseEvents); }); // 画笔渐变(用于羽化边缘) var grad = drawon_ctx.createRadialGradient(bsh, bsh, 0, bsh, bsh, bsh); grad.addColorStop(0, "rgba(255,255,255,1)"); grad.addColorStop(1, "rgba(255,255,255,0)"); var v_brush = createCanvas(bs); // 创建离屏画布 function createCanvas(w, h) { h = h || w; var c = document.createElement("canvas"); c.width = w; c.height = h; c.ctx = c.getContext("2d"); return c; } // 从目标位置提取画笔内容 function brushFrom(tmp_ctx, x, y) { // 先清空画笔画布 v_brush.ctx.clearRect(0, 0, bs, bs); // 提取目标区域像素,只保留不透明部分 v_brush.ctx.globalCompositeOperation = "source-over"; v_brush.ctx.globalAlpha = 1; v_brush.ctx.drawImage(tmp_canvas, -(x - bsh), -(y - bsh)); // 使用白色渐变蒙版,只保留原有像素的透明度信息,避免黑色底色干扰 v_brush.ctx.globalCompositeOperation = "destination-in"; v_brush.ctx.fillStyle = grad; v_brush.ctx.fillRect(0, 0, bs, bs); // 切换回正常合成模式,确保后续绘制正确 v_brush.ctx.globalCompositeOperation = "source-over"; } // 初始化变量 var w = canvas.width; var h = canvas.height; var cw = w / 2; var ch = h / 2; var lastX; var lastY; // 创建初始背景 function createBackground() { tmp_canvas.width = w; tmp_canvas.height = h; doFor(64, function () { tmp_ctx.fillStyle = `rgb(${randI(255)},${randI(255)},${randI(255)})`; tmp_ctx.fillRect(randI(w), randI(h), randI(10, 100), randI(10, 100)); }); } // 主更新函数 function update(timer) { drawon_ctx.setTransform(1, 0, 0, 1, 0, 0); drawon_ctx.globalAlpha = 1; if (w !== innerWidth || h !== innerHeight) { cw = (w = canvas.width = innerWidth) / 2; ch = (h = canvas.height = innerHeight) / 2; createBackground(); } else { drawon_ctx.clearRect(0, 0, w, h); } drawon_ctx.drawImage(tmp_canvas, 0, 0); if (mouse.button) { v_brush.ctx.globalAlpha = smudgeAmount; var dx = mouse.x - lastX; var dy = mouse.y - lastY; var dist = Math.sqrt(dx * dx + dy * dy); for (var i = 0; i < dist; i += 1) { var ni = i / dist; brushFrom(tmp_ctx, lastX + dx * ni, lastY + dy * ni); ni = (i + 1) / dist; // 使用"source-atop"合成模式,只在已有像素上绘制,避免透明区域被污染 tmp_ctx.globalCompositeOperation = "source-atop"; tmp_ctx.drawImage(v_brush, lastX + dx * ni - bsh, lastY + dy * ni - bsh); tmp_ctx.globalCompositeOperation = "source-over"; } } else { v_brush.ctx.clearRect(0, 0, bs, bs); } lastX = mouse.x; lastY = mouse.y; requestAnimationFrame(update); } requestAnimationFrame(update); </script>
关键修改点
- 画笔渐变调整:将原黑色渐变改为白色渐变,避免采样时引入黑色底色
- 合成模式优化:
- 在
brushFrom函数中,先清空画笔画布再采样 - 绘制涂抹内容时使用
source-atop合成模式,只在已有可见像素上叠加,不影响透明区域
- 在
- 变量逻辑简化:调整
randI函数的参数处理,让代码更清晰
内容的提问来源于stack exchange,提问作者Alsat
相关产品推荐
相关产品推荐

