Canvas painting that fades with time | Strange alpha behavior

I draw on a canvas that doesnโ€™t clean and makes it so that the canvas either fades to solid color over time or fades to alpha, revealing the layer behind.

My first instinct was to simply fill in the rectangle above the low alpha pattern with each frame so that the fill color builds up, gradually reducing the picture.

But I found some strange behavior (at least for me, I'm sure there is a reason). The fill color never fully accumulates. And the results change depending on the color of the paint and the fill, lighter / darker than each other.

I found this question when someone did the same thing as me: do the lines disappear after drawing the canvas?

The top answer looks good, and it's the same as me. BUT it only works with black and white. Here is another version of the same violin with different colors, you will see that the picture never disappears, it leaves a ghost: 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();

Also, if you change the opacity of the fill to 0.01, and the time to something like 20 ms, it will not even fill the correct color, leaving it gray.

Other things I've tried suffer from the same root problem. I tried to jump between two canvases, taking canvas A and drawing it with reduced alpha to canvas B, before drawing canvas B back onto the canvas. A - the same problem, there is a threshold where it does not disappear.

, - 0,95, . - , - ( - 10):

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

, imageData , .

- , !

  • oh , , /, , , . !
+4
4

RGB 8- !

RGB, , 8- , . (8- ) 14 * 0,1 = 1, 8 * 0,1 = 1 , , , , .

, -, "destination-out". , .

globalAlpha = 0,01 0,006, . , , .

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

.

.

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);
Hide result
+3

Blindman67, , , . , , , .

, , , - , :
...

, , , , - , OP.

, , :

  • ,
  • ((currentTime - object.calledTime) / duration)
  • if alpha <= 0,

// 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>
Hide result

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>
Hide result
+1

, - , , - , , , , - , , .

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

The answers here really helped me understand the problem. I tried this @ Blindman67 but had problems with the method globalCompositeOperationas mentioned above.

What I ended up with is the push()mouse coordinates in the array, and then the shift()array when the row gets as many as I want this trail to be.

Then each renderAnimationFrameI draw a set of segments in increasing 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>
Run codeHide result
0
source

Source: https://habr.com/ru/post/1665863/


All Articles