首页 文章

画布中的绘画随着时间的推移逐渐消失奇怪的alpha分层行为

提问于
浏览
5

我正在绘制一幅未被清除的画布,并使其随着时间的推移逐渐变为纯色,或者在显示背后的图层时淡化 .

我的第一直觉是简单地在图纸上填充一个矩形,每个框架都有一个低alpha,这样填充颜色就会逐渐淡出画面 .

但我发现了一些奇怪的行为(至少对我而言,我确信这是有原因的) . 填充颜色永远不会完全累积 . 结果的变化取决于油漆颜色和填充颜色相互之间的颜色更浅/更暗 .

我发现这个问题,有人和我一样:fade out lines after drawing canvas?

最佳答案看起来不错,它是同一个小提琴的另一个版本,有不同的颜色,你会看到绘画永远不会消失,它会留下一个鬼:http://jsfiddle.net/R4V97/92/

var canvas = document.getElementById("canvas"),
    ctx = canvas.getContext("2d"),
    painting = false,
    lastX = 0,
    lastY = 0;

canvas.width = canvas.height = 600;

canvas.onmousedown = function (e) {
    if (!painting) {
        painting = true;
    } else {
        painting = false;
    }

    lastX = e.pageX - this.offsetLeft;
    lastY = e.pageY - this.offsetTop;
};

canvas.onmousemove = function (e) {
    if (painting) {
        mouseX = e.pageX - this.offsetLeft;
        mouseY = e.pageY - this.offsetTop;

        ctx.strokeStyle = "rgba(255,255,255,1)";
        ctx.beginPath();
        ctx.moveTo(lastX, lastY);
        ctx.lineTo(mouseX, mouseY);
        ctx.stroke();

        lastX = mouseX;
        lastY = mouseY;
    }
}

function fadeOut() {
    ctx.fillStyle = "rgba(60,30,50,0.2)";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    setTimeout(fadeOut,100);
}

fadeOut();

此外,如果您将填充不透明度更改为0.01,并将时间更改为20毫秒,它甚至不会填充正确的颜色,使其保持灰色 .

我尝试过的其他事情都遭受同样的根本问题 . 我尝试在两个画布之间弹跳,画布A并用画布B缩小alpha到画布B,然后将画布B画回画布A - 同样的问题,它有一个不会消失的阈值 .

作为测试,我甚至尝试了获取图像数据的超慢速,循环遍历所有像素alpha通道并在将数据放回之前乘以0.95 . 它仍然留下一个幽灵,我必须在循环中做这样的事情(由于某种原因它甚至从未低于10):

if (alpha<25) {
    alpha = 0;
}

我想我可以将画布划分为一个网格或行,并将imageData的事情做为每帧一个单元格,低衰落时间可能不会引人注意 .

但是,如果有人知道更好的方式,或者我没有得到的核心事情,我将非常感激!

  • 哦也应该注意,我用粒子/算法绘画,所以我不是在寻找解决方案,这意味着我要不断刷新并重新绘制相同的点 . 谢谢!

4 回答

  • 3

    回答我自己的问题与我最终的结果 - 感谢回答,在得知核心问题是一个舍入问题后,我认为在淡入淡出量中添加一些随机噪声可能有助于确保它不总是四舍五入到相同的数字,有点像它被卡住时摇一摇 .

    这是相同的jsfiddle修改:http://jsfiddle.net/R4V97/97/

    var canvas = document.getElementById("canvas"),
        ctx = canvas.getContext("2d"),
        painting = false,
        lastX = 0,
        lastY = 0;
    
    canvas.width = canvas.height = 600;
    
    canvas.onmousedown = function (e) {
        if (!painting) {
            painting = true;
        } else {
            painting = false;
        }
    
        lastX = e.pageX - this.offsetLeft;
        lastY = e.pageY - this.offsetTop;
    };
    
    canvas.onmousemove = function (e) {
        if (painting) {
            mouseX = e.pageX - this.offsetLeft;
            mouseY = e.pageY - this.offsetTop;
    
            ctx.strokeStyle = "rgba(255,255,255,1)";
            ctx.beginPath();
            ctx.moveTo(lastX, lastY);
            ctx.lineTo(mouseX, mouseY);
            ctx.stroke();
    
            lastX = mouseX;
            lastY = mouseY;
        }
    }
    
    function fadeOut() {
        var r = 0.3 + (Math.random()*0.1);
        ctx.fillStyle = "rgba(60,30,50,"+r+")";
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        setTimeout(fadeOut,100);
    }
    
    fadeOut();
    

    这略微损害了褪色的平滑度,但它比幽灵痕迹更不明显/侵入 .

  • 1

    RGB和8位整数数学!

    您需要避免触摸RGB通道,因为当您对8位值进行数学运算时,结果将出现巨大错误 . 例如(8位整数数学)14 * 0.1 = 1,8 * 0.1 = 1因此,当您绘制现有像素时,您将获得每个通道的舍入误差,具体取决于您在顶部绘制的颜色 .

    没有完美的解决方案但是你可以通过使用全局复合操作“destination-out”来避免颜色通道并仅淡化alpha通道 . 这将通过减少像素alpha来淡出渲染 .

    适用于衰减率低至globalAlpha = 0.01甚至更低0.006,但在此之下可能会很麻烦 . 然后,如果您需要更慢的淡入淡出,则每隔2帧或3帧进行淡入淡出 .

    ctx.globalAlpha = 0.01;           // fade rate
    ctx.globalCompositeOperation = "destination-out"  // fade out destination pixels
    ctx.fillRect(0,0,w,h)
    ctx.globalCompositeOperation = "source-over"
    ctx.globalAlpha = 1;           // reset alpha
    

    请注意,这会将画布淡化为透明 . 如果您希望淡入淡出朝着特定颜色前进,则需要将淡化画布保持为单独的屏幕外画布,并将其绘制在具有所需背景的画布上以淡入淡出 .

    演示在彩色背景上的彩色粒子与淡入淡出 .

    var canvas = document.createElement("canvas");
    canvas.width = 1024;
    canvas.height = 1024;
    var ctx = canvas.getContext("2d");
    var w = canvas.width;
    var h = canvas.height;
    document.body.appendChild(canvas);
    
    var fadCan = document.createElement("canvas");
    fadCan.width = canvas.width;
    fadCan.height = canvas.height;
    var fCtx = fadCan.getContext("2d");
    
    var cw = w / 2;  // center 
    var ch = h / 2;
    var globalTime;
    
    function randColour(){
        return "hsl("+(Math.floor(Math.random()*360))+",100%,50%)";
    }
    var pps = [];
    for(var i = 0; i < 100; i ++){
        pps.push({
            x : Math.random() * canvas.width,
            y : Math.random() * canvas.height,
            d : Math.random() * Math.PI * 2,
            sp : Math.random() * 2 + 0.41,
            col : randColour(),
            s : Math.random() * 5 + 2,
            t : (Math.random() * 6 -3)/10,
            
        });
    }
    function doDots(){
        for(var i = 0; i < 100; i ++){
            var d = pps[i];
            d.d += d.t * Math.sin(globalTime / (d.t+d.sp+d.s)*1000);
            d.x += Math.cos(d.d) * d.sp;
            d.y += Math.sin(d.d) * d.sp;
            d.x = (d.x + w)%w;
            d.y = (d.y + w)%w;
            fCtx.fillStyle = d.col;
            fCtx.beginPath();
            fCtx.arc(d.x,d.y,d.s,0,Math.PI * 2);
            fCtx.fill();
            
        }
    }
    
    
    var frameCount = 0;
    // main update function
    function update(timer){
        globalTime = timer;
        frameCount += 1;
        ctx.setTransform(1,0,0,1,0,0); // reset transform
        ctx.globalAlpha = 1;           // reset alpha
        ctx.fillStyle = "hsl("+(Math.floor((timer/50000)*360))+",100%,50%)";
        ctx.fillRect(0,0,w,h);
        doDots();
        if(frameCount%2){
            fCtx.globalCompositeOperation = "destination-out";
            fCtx.fillStyle = "black";
            var r = Math.random() * 0.04
            fCtx.globalAlpha = (frameCount & 2 ? 0.16:0.08)+r;
            fCtx.fillRect(0,0,w,h);
            fCtx.globalAlpha = 1;
            fCtx.globalCompositeOperation = "source-over"
        }
        ctx.drawImage(fadCan,0,0)
        requestAnimationFrame(update);
    }
    requestAnimationFrame(update);
    

    在色的背景的演示图画与退色 .

    单击拖动鼠标进行绘制 .

    var canvas = document.createElement("canvas");
    canvas.width = 1024;
    canvas.height = 1024;
    var ctx = canvas.getContext("2d");
    var w = canvas.width;
    var h = canvas.height;
    document.body.appendChild(canvas);
    
    var fadCan = document.createElement("canvas");
    fadCan.width = canvas.width;
    fadCan.height = canvas.height;
    var fCtx = fadCan.getContext("2d");
    
    var cw = w / 2;  // center 
    var ch = h / 2;
    var globalTime;
    
    function randColour(){
        return "hsl("+(Math.floor(Math.random()*360))+",100%,50%)";
    }
    
    
    
    // main update function
    function update(timer){
        globalTime = timer;
        ctx.setTransform(1,0,0,1,0,0); // reset transform
        ctx.globalAlpha = 1;           // reset alpha
        ctx.fillStyle = "hsl("+(Math.floor((timer/150000)*360))+",100%,50%)";
        ctx.fillRect(0,0,w,h);
        if(mouse.buttonRaw === 1){
            fCtx.strokeStyle = "White";
            fCtx.lineWidth = 3;
            fCtx.lineCap = "round";
            fCtx.beginPath();
            fCtx.moveTo(mouse.lx,mouse.ly);
            fCtx.lineTo(mouse.x,mouse.y);
            fCtx.stroke();
        }
    
    
        mouse.lx = mouse.x;
        mouse.ly = mouse.y;
        fCtx.globalCompositeOperation = "destination-out";
        fCtx.fillStyle = "black";
        fCtx.globalAlpha = 0.1;
        fCtx.fillRect(0,0,w,h);
        fCtx.globalAlpha = 1;
        fCtx.globalCompositeOperation = "source-over"
        ctx.drawImage(fadCan,0,0)
        requestAnimationFrame(update);
    }
    
    
    var mouse = (function () {
        function preventDefault(e) {
            e.preventDefault();
        }
        var mouse = {
            x : 0,
            y : 0,
            w : 0,
            alt : false,
            shift : false,
            ctrl : false,
            buttonRaw : 0,
            over : false,
            bm : [1, 2, 4, 6, 5, 3],
            active : false,
            bounds : null,
            crashRecover : null,
            mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
        };
        var m = mouse;
        function mouseMove(e) {
            var t = e.type;
            m.bounds = m.element.getBoundingClientRect();
            m.x = e.pageX - m.bounds.left + scrollX;
            m.y = e.pageY - m.bounds.top + scrollY;
            m.alt = e.altKey;
            m.shift = e.shiftKey;
            m.ctrl = e.ctrlKey;
            if (t === "mousedown") {
                m.buttonRaw |= m.bm[e.which - 1];
            } else if (t === "mouseup") {
                m.buttonRaw &= m.bm[e.which + 2];
            } else if (t === "mouseout") {
                m.buttonRaw = 0;
                m.over = false;
            } else if (t === "mouseover") {
                m.over = true;
            } else if (t === "mousewheel") {
                m.w = e.wheelDelta;
            } else if (t === "DOMMouseScroll") {
                m.w = -e.detail;
            }
            if (m.callbacks) {
                m.callbacks.forEach(c => c(e));
            }
            if ((m.buttonRaw & 2) && m.crashRecover !== null) {
                if (typeof m.crashRecover === "function") {
                    setTimeout(m.crashRecover, 0);
                }
            }
            e.preventDefault();
        }
        m.addCallback = function (callback) {
            if (typeof callback === "function") {
                if (m.callbacks === undefined) {
                    m.callbacks = [callback];
                } else {
                    m.callbacks.push(callback);
                }
            }
        }
        m.start = function (element) {
            if (m.element !== undefined) {
                m.removeMouse();
            }
            m.element = element === undefined ? document : element;
            m.mouseEvents.forEach(n => {
                m.element.addEventListener(n, mouseMove);
            });
            m.element.addEventListener("contextmenu", preventDefault, false);
            m.active = true;
        }
        m.remove = function () {
            if (m.element !== undefined) {
                m.mouseEvents.forEach(n => {
                    m.element.removeEventListener(n, mouseMove);
                });
                m.element.removeEventListener("contextmenu", preventDefault);
                m.element = m.callbacks = undefined;
                m.active = false;
            }
        }
        return mouse;
    })();
    
    mouse.start(canvas);
    requestAnimationFrame(update);
    
  • 3

    Blindman67的回答可能确实给出了为什么会发生这种情况的正确核心原因 . 但不幸的是,我认为他的解决方案也不会奏效 .

    实际上,我能想到的唯一真正的解决方案就是你不想要的解决方案:
    Record all the points of your paths and draw it one by one...

    所以即使你说你不想要这个解决方案,我也会在这里发布,以防它可以帮助别人而不是OP .

    此示例确实保存了路径,但您可以使用相同的基本步骤保存任何需要随时间消失的对象:

    • 记录对象的被叫时间

    • ((currentTime - object.calledTime) / duration) 获取alpha

    • 如果 alpha <= 0 ,删除该对象

    • else设置alpha并重绘

    // Some constructors
    
    // The main Object that will handle all our paths + drawing logics
    //  Expects a main (e.g visible) context as only argument
    function PathFader(mainContext) {
      this.mainContext = mainContext;
      // create a copy of the main canvas
      this.ctx = mainContext.canvas.cloneNode().getContext('2d');
      this.list = [];
      // here are some settings you can change
      this.duration = 4000; // the time it takes to fade out a single path
      this.ctx.strokeStyle = 'white'; // the color of our paths
    };
    PathFader.prototype = Object.create({
      add: function(lx, ly, nx, ny) {
        this.list.push(new Path(lx, ly, nx, ny));
      },
      remove: function(path) {
        var index = this.list.indexOf(path);
        this.list.splice(index, 1);
      },
      draw: function(time) {
        // first set the currentTime to the one passed by rAF
        this.currentTime = time;
        // clear the curretn state
        this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
        // redraw all our pathes
        this.list.forEach(this.drawPathes, this);
        // draw our path context to the main one
        this.mainContext.drawImage(this.ctx.canvas, 0, 0);
      },
      drawPathes: function(path, i, list) {
        // calculate the path alpha at this time
        var a = 1 - ((this.currentTime - path.time) / this.duration);
        // if we're transparent
        if (a < 0) {
          this.remove(path);
          return;
        }
        // otherwise set the alpha
        this.ctx.globalAlpha = a;
        // draw the path
        this.ctx.beginPath();
        this.ctx.moveTo(path.lastX, path.lastY);
        this.ctx.lineTo(path.nextX, path.nextY);
        this.ctx.stroke();
      },
      resize: function() {
        var strokeStyle = this.ctx.strokeStyle,
          lineWidth = this.ctx.lineWidth;
        this.ctx.canvas.width = this.mainContext.canvas.width;
        this.ctx.canvas.height = this.mainContext.canvas.height;
        this.ctx.strokeStyle = strokeStyle;
        this.ctx.lineWidth = lineWidth;
      }
    });
    
    function Path(lastX, lastY, nextX, nextY) {
      this.time = performance.now();
      this.lastX = lastX;
      this.lastY = lastY;
      this.nextX = nextX;
      this.nextY = nextY;
    }
    
    var canvas = document.getElementById("canvas"),
      ctx = canvas.getContext("2d");
    var painting = false,
      lastX = 0,
      lastY = 0,
      nextX, nextY,
      pathFader = new PathFader(ctx);
    
    canvas.width = canvas.height = 600;
    // since we do set the width and height of the mainCanvas after,
    // we have to resize the Pathes canvas too
    pathFader.resize();
    
    
    canvas.onmousedown = function(e) {
      painting = !painting;
      lastX = e.pageX - this.offsetLeft;
      lastY = e.pageY - this.offsetTop;
    };
    
    // Since this is more performance consumptive than the original code,
    //  we'll throttle the mousemove event
    
    var moving = false;
    canvas.onmousemove = function throttleMouseMove(e) {
      if (!moving) {
        nextX = e.pageX - this.offsetLeft;
        nextY = e.pageY - this.offsetTop;
        requestAnimationFrame(handleMouseMove);
        moving = true;
      }
    };
    
    function handleMouseMove() {
      moving = false;
      if (painting) {
        // add a new path, don't draw anything yet
        pathFader.add(lastX, lastY, nextX, nextY);
    
        lastX = nextX;
        lastY = nextY;
      }
    }
    
    ctx.fillStyle = "rgb(60,30,50)";
    
    function anim(time) {
      // draw our background
      ctx.fillRect(0, 0, canvas.width, canvas.height);
      // draw the pathes (remember to pass rAF time param !)
      pathFader.draw(time);
      // do it again at next screen refresh
      requestAnimationFrame(anim);
    }
    
    anim();
    
    <canvas id="canvas"></canvas>
    

    Ps:另一种解决方案是使用大量的画布,并在下一个画布上绘制第一个不透明度较低的画布 .

    这是一个概念证明,相对于持续时间控制有一些错误......

    var ctx = canvas.getContext('2d');
    var objects = [],
      w = canvas.width,
      h = canvas.height;
    
    function Fader(mainContext) {
    
      var nbOfFrames = 25;
      this.distance = 2000;
    
      this.mainContext = mainContext;
      this.list = [mainContext];
      var ctx;
      var alphaStep = 1 - (1 / (nbOfFrames - 1));
    
      for (var i = 0; i < nbOfFrames; i++) {
        ctx = mainContext.canvas.cloneNode().getContext('2d');
        this.list.push(ctx);
        ctx.globalAlpha = 1 - (i / (nbOfFrames + 1));
      }
    }
    Fader.prototype = {
      draw: function() {
        var main = this.list[0];
        if (!this.creationTime) {
          this.creationTime = performance.now();
          return;
        }
        // only used at init, to set the distance between each frame,
        // but there is something wrong here..
        var limit = ~~(((performance.now() - this.creationTime) / this.distance) * this.list.length);
        if (!limit) {
          return;
        } // first frame
    
        var c;
        // update the contexts content
        for (var i = Math.min(this.list.length - 1, limit); i > 0; i--) {
          c = this.list[i];
          c.clearRect(0, 0, w, h);
          c.drawImage(this.list[i - 1].canvas, 0, 0);
        }
        // draw them back to the main one
        main.globalCompositeOperation = 'destination-over';
        this.list.forEach(function(c, i) {
          if (!i) return;
          main.drawImage(c.canvas, 0, 0);
        });
        main.globalCompositeOperation = 'source-over';
      }
    };
    
    var fader = new Fader(ctx);
    
    // taken from http://stackoverflow.com/a/23486828/3702797
    for (var i = 0; i < 100; i++) {
      objects.push({
        angle: Math.random() * 360,
        x: 100 + (Math.random() * w / 2),
        y: 100 + (Math.random() * h / 2),
        radius: 10 + (Math.random() * 40),
        speed: 1 + Math.random() * 20
      });
    }
    
    var stopMoving = false;
    document.body.onclick = e => stopMoving = !stopMoving;
    
    ctx.fillStyle = "rgb(60,30,50)";
    var draw = function() {
    
      ctx.clearRect(0, 0, w, h);
    
      for (var n = 0; n < 100; n++) {
        var entity = objects[n],
          velY = stopMoving ? 0 : Math.cos(entity.angle * Math.PI / 180) * entity.speed,
          velX = stopMoving ? 0 : Math.sin(entity.angle * Math.PI / 180) * entity.speed;
    
        entity.x += velX;
        entity.y -= velY;
    
        ctx.drawImage(img, entity.x, entity.y, entity.radius, entity.radius);
    
        entity.angle++;
      }
    
      fader.draw();
      ctx.globalCompositeOperation = 'destination-over';
      ctx.fillRect(0,0,w, h);
      ctx.globalCompositeOperation = 'source-over';
      requestAnimationFrame(draw);
    
    }
    var img = new Image();
    img.onload = draw;
    img.crossOrigin = 'anonymous';
    img.src = "https://dl.dropboxusercontent.com/s/4e90e48s5vtmfbd/aaa.png";
    
    <canvas id="canvas" width=600 height=600></canvas>
    
  • 0

    这里的答案真的帮助我理解了这个问题 . 我尝试了@ Blindman67的方式,但与其他人提到的 globalCompositeOperation 方法有问题 .

    我最终做的是将 push() 鼠标坐标放入一个数组中,然后 shift() 该数组在该行获得时与我一样长想要追踪 .

    然后,每个 renderAnimationFrame 我以递增透明度绘制一组段 .

    var canvas = document.getElementById('noGhost'),
    ctx = canvas.getContext('2d'),
    time = 0,
    segments = [],
    maxLength = 20,
    lineColor = {
      r: 255,
      g: 0,
      b: 0
    };
    //really nice options for hex to rgb here: https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
    
    
    document.addEventListener('mousemove', function(evt){
      segments.push({
      x: evt.pageX,
      y: evt.pageY,
      });
      
      if(segments.length > maxLength) {
        segments.shift();
      }
    }, false);
    
    
    function render() {
      //reset canvas
      canvas.width = canvas.width;
      
      if(segments.length > 2) {
        for(var i = 1; i < segments.length; i++) {
          ctx.beginPath();
          ctx.strokeStyle = "rgba(" + lineColor.r + "," + lineColor.g + "," + lineColor.b + "," + (i / segments.length) + ")"
          ctx.moveTo(segments[i-1].x, segments[i-1].y);
          ctx.lineTo(segments[i].x, segments[i].y);
          ctx.stroke();
        }
        
        
      }
      //as time goes, shorten the length of the line
      time++;
      if(time % 2 == 0) {
      segments.shift();
      }
      requestAnimationFrame(render);
    };
    requestAnimationFrame(render);
    
    #noGhost {
      background: silver;
    }
    
    <canvas height=200 width=400 id="noGhost">
    </canvas>
    

相关问题