Painting in Canvas which fades with time | Strange alpha layering behaviour

前端 未结 4 2109
旧时难觅i
旧时难觅i 2020-12-18 05:58

I\'m painting to a canvas which isn\'t being cleared and making it so that the canvas either fades to a solid colour over time, or fades in alpha revealing the layer behind.

相关标签:
4条回答
  • 2020-12-18 06:32

    The answers here really helped me to understand the problem. I tried it @Blindman67's way but had issues with the globalCompositeOperation method as others mentioned.

    What I ended up doing is push() mouse coordinates into an array, and then shift() the array when the line gets as long as I want the trail to be.

    Then, each renderAnimationFrame I am drawing the set of segments in ascending transparency.

    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>

    0 讨论(0)
  • 2020-12-18 06:39

    Answering my own question with what I ended up going with - thanks to the responses, after learning that the core problem is a rounding issue I figured adding some random noise to the fade amount could help make sure it's not always rounding to the same number, kinda like giving it a shake when it's stuck.

    Here's that same jsfiddle modified: 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();
    

    This slightly compromises the smoothness of the fade, but it's a lot less noticeable/intrusive than the ghost trails.

    0 讨论(0)
  • 2020-12-18 06:42

    Blindman67's answer probably does give a correct core reason to why this is happening. But unfortunately, I think his solution won't work either.

    Actually, the only real solution I can think of is one that you didn't wanted :
    Record all the points of your paths and draw it one by one...

    So even if you said you didn't want this solution, I'll post it here in case it can help someone else than OP.

    this example does save paths, but you could save any object that needs to be faded over time with just the same basic steps :

    • record object's called time
    • get the alpha with ((currentTime - object.calledTime) / duration)
    • if alpha <= 0, remove the object
    • else set the alpha and redraw

    // 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 : An alternative solution would be to use a lot of canvases, and draw the first ones on the next ones with less and less opacity.

    Here is a proof of concept which has some bugs relative to the duration controls...

    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 讨论(0)
  • 2020-12-18 06:48

    RGB and 8bit integer math!

    You need to avoid touching the RGB channels because when you do math on 8 bit values the results will have a huge error. Eg (8bit integer math) 14 * 0.1 = 1, 8 * 0.1 = 1 Thus when you draw over the existing pixels you will get a rounding error that will be different for each channel depending on the colour you are drawing on top.

    There is not perfect solution but you can avoid the colour channels and fade only the alpha channel by using the global composite operation "destination-out" This will fade out the rendering by reducing the pixels alpha.

    Works well for fade rates down to globalAlpha = 0.01 and even a little lower 0.006 but it can be troublesome below that. Then if you need even slower fade just do the fade every 2nd or 3rd frame.

    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
    

    Please note that this fade the canvas to transparent. If you want the fade to progress towards a particular colour you need to keep the fading canvas as a separate offscreen canvas and draw it over a canvas with the desired background to fade to.

    Demo coloured particles on coloured background with fade.

    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);

    Demo drawing on coloured background with fade.

    Click drag mouse to draw.

    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);

    0 讨论(0)
提交回复
热议问题