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