Black resized canvas not completely fading drawings to black over time

I have a black canvas with things being drawn inside it. I want the things drawn inside to fade to black, over time, in the order at which they are drawn (FIFO). This works if I use a canvas which hasn't been resized. When the canvas is resized, the elements fade to an off-white.

Question: Why don't the white specks fade completely to black when the canvas has been resized? How can I get them to fade to black in the same way that they do when I haven't resized the canvas?

Here's some code which demonstrates. http://jsfiddle.net/6VvbQ/35/

var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.fillRect(0, 0, 300, 150);

// Comment this out and it works as intended, why?
canvas.width = canvas.height = 300;

window.draw = function () {
    context.fillStyle = 'rgba(255,255,255,1)';
    context.fillRect(
        Math.floor(Math.random() * 300),
        Math.floor(Math.random() * 150),
        2, 2);
    context.fillStyle = 'rgba(0,0,0,.02)';
    context.fillRect(0, 0, 300, 150);

    setTimeout('draw()', 1000 / 20);
}
setTimeout('draw()', 1000 / 20);

Answers:

Answer

The problem is two-parted:

  1. There is a (rather known) rounding error when you draw with low alpha value. The browser will never be able to get the resulting mix of the color and alpha channel equal to 0 as the resulting float value that is mixed will be converted to integer at the time of drawing which means the value will never become lower than 1. Next time it mixes it (value 1, as alpha internally is a value between 0 and 255) will use this value again and it get rounded to again to 1, and forever it goes.

  2. Why it works when you have a resized canvas - in this case it is because you are drawing only half the big canvas to the smaller which result in the pixels being interpolated. As the value is very low this means in this case the pixel will turn "black" (fully transparent) as the average between the surrounding pixels will result in the value being rounded to 0 - sort of the opposite than with #1.

To get around this you will manually have to clear the spec when it is expected to be black. This will involve tracking each particle/spec yourselves or change the alpha using direct pixel manipulation.

Update:

The key is to use tracking. You can do this by creating each spec as a self-updating point which keeps track of alpha and clearing.

Online demo here

A simple spec object can look like this:

function Spec(ctx, speed) {

    var me = this;

    reset();                             /// initialize object

    this.update = function() {

        ctx.clearRect(me.x, me.y, 1, 1); /// clear previous drawing

        this.alpha -= speed;             /// update alpha

        if (this.alpha <= 0) reset();    /// if black then reset again

        /// draw the spec
        ctx.fillStyle = 'rgba(255,255,255,' + me.alpha + ')';
        ctx.fillRect(me.x, me.y, 1, 1);
    }

    function reset() {

        me.x = (ctx.canvas.width * Math.random())|0;  /// random x rounded to int
        me.y = (ctx.canvas.height * Math.random())|0; /// random y rounded to int

        if (me.alpha) {                               /// reset alpha
            me.alpha = 1.0;                           /// set to 1 if existed
        } else {
            me.alpha = Math.random();                 /// use random if not
        }
    }
}

Rounding the x and y to integer values saves us a little when we need to clear the spec as we won't run into sub-pixels. Otherwise you would need to clear the area around the spec as well.

The next step then is to generate a number of points:

/// create 100 specs with random speed
var i = 100, specs = [];

while(i--) {
    specs.push(new Spec(ctx, Math.random() * 0.015 + 0.005));
}

Instead of messing with FPS you simply use the speed which can be set individually per spec.

Now it's simply a matter of updating each object in a loop:

function loop() {

    /// iterate each object
    var i = specs.length - 1;    
    while(i--) {
        specs[i].update();        /// update each object
    }

    requestAnimationFrame(loop);  /// loop synced to monitor
}

As you can see performance is not an issue and there is no residue left. Hope this helps.

Answer

I don't know if i have undertand you well but looking at you fiddle i think that, for what you are looking for, you need to provide the size of the canvas in any iteration of the loop. If not then you are just taking the initial values:

EDIT

You can do it if you apply a threshold filter to the canvas. You can run the filter every second only just so the prefromanece is not hit so hard.

var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.fillRect(0,0,300,150);
//context.globalAlpha=1;
//context.globalCompositeOperation = "source-over";


var canvas2 = document.getElementById('canvas2');
var context2 = canvas2.getContext('2d');

canvas2.width=canvas2.height=canvas.width;

window.draw = function(){
    var W = canvas2.width;
    var H = canvas2.height;
    context2.fillStyle='rgba(255,255,255,1)';
    context2.fillRect(
        Math.floor(Math.random()*W),
        Math.floor(Math.random()*H),
        2,2);
    context2.fillStyle='rgba(0,0,0,.02)';
    context2.fillRect(0,0,W,H);

    context.fillStyle='rgba(0,0,0,1)';
    context.fillRect(0,0,300,150);
    context.drawImage(canvas2,0,0,300,150);

    setTimeout('draw()', 1000/20);
}
setTimeout('draw()', 1000/20);

window.thresholdFilter = function () {
    var W = canvas2.width;
    var H = canvas2.height;
    var i, j, threshold = 30, rgb = []
    ,   imgData=context2.getImageData(0,0,W,H), Npixels = imgData.data.length;

    for (i = 0; i < Npixels; i += 4) {
        rgb[0] = imgData.data[i];
        rgb[1] = imgData.data[i+1];
        rgb[2] = imgData.data[i+2];
        if (    rgb[0] < threshold &&
                rgb[1] < threshold &&
                rgb[2] < threshold
        ) {
           imgData.data[i] = 0;
           imgData.data[i+1] = 0;
           imgData.data[i+2] = 0;
        }
    }

    context2.putImageData(imgData,0,0);
};

setInterval("thresholdFilter()", 1000);

Here is the fiddle: http://jsfiddle.net/siliconball/2VaLb/4/

Answer

To avoid the rounding problem you could extract the fade effect to a separate function with its own timer, using longer refresh interval and larger alpha value.

var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.fillRect(0, 0, 300, 150);

// Comment this out and it works as intended, why?
canvas.width = canvas.height = 300;

window.draw = function () {
    context.fillStyle = 'rgba(255,255,255,1)';
    context.fillRect(
        Math.floor(Math.random() * 300),
        Math.floor(Math.random() * 300),
        2, 2);
    setTimeout('draw()', 1000 / 20);
}

window.fadeToBlack = function () {
    context.fillStyle = 'rgba(0,0,0,.1)';
    context.fillRect(0, 0, 300, 300);
    setTimeout('fadeToBlack()', 1000 / 4);    
}

draw();
fadeToBlack();

Fiddle demonstrating this: http://jsfiddle.net/6VvbQ/37/

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us Javascript

©2020 All rights reserved.