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

如何在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>

关键修改点

  1. 画笔渐变调整:将原黑色渐变改为白色渐变,避免采样时引入黑色底色
  2. 合成模式优化:
    • 在brushFrom函数中,先清空画笔画布再采样
    • 绘制涂抹内容时使用source-atop合成模式,只在已有可见像素上叠加,不影响透明区域
  3. 变量逻辑简化:调整randI函数的参数处理,让代码更清晰

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 20:25:30