Fade Effect - What's with the ghost?

My intention is to create a fade effect for thousands of shapes on the canvas (They appear, and then fade into blackness).
This is the solution I came up with, and have found other people using.

void draw()
{
    fill(0, 10);
    rect(0, 0, width, height);
}

The problem is that the shapes never fade away completely. The lower the alpha (slower the fade), the brighter the ghost that is left behind. Here’s a sample sketch that illustrates my problem. Click to mouse to create more ellipses.
Can anybody explain to me why the objects stop fading?

void setup()
{
  size(500, 500);
  background(0);
  ellipse(width/2, height/2, 200, 200);
}

void draw()
{
  noStroke();
  fill(0, 10);
  rect(0, 0, width, height);
  
  if(mousePressed)
  {
    fill(255);
    ellipse(mouseX, mouseY, 100, 100);
  }
}
1 Like

Hello!
I ran your code in the Processing IDE and in the openprocessing.org online editor.
In both cases the ellipses faded completely to black…

Really??
That’s super interesting. I just tried on openprocessing.org and still have the same problem. Though I just noticed now that it’s way more apparent on my hdmi monitor than my laptop’s, so maybe the screen has something to do with that.
This is an image of the issue, if you can’t see the dots, than it must me a monitor thing. I really hope someone can help me with this, I have no idea how to troubleshoot it further.
ghost dots

1 Like

Hello,

I can see the circle in the black; you can see this effect better if you invert colors.
I wrote a snippet of code to plot the color over time; you can see the values in the console.

void setup()
  {
  size(250, 500);
  background(255);
  noStroke();
  fill(0);
  //rect(0, 0, width, height);
  ellipse(width/2, 3*height/4, 200, 200);
  noStroke();
  }

void draw()
  {
  noStroke();
  //push();
  fill(255, 10); //Try different values here; 1 gives a stragiht line
  rect(0, 250, width, height);
  //pop();
  int col = get(width/2, 3*height/4);
  println(hex(col), (red(col)));
  
  push();
  strokeWeight(2);
  stroke(0);
  point(x, red(col));
  x++;
  if(x >= width) noLoop();
  pop();

  if(mousePressed)
    {
    fill(0);
    ellipse(mouseX, mouseY, 100, 100);
    }
  }

:)

2 Likes

Thanks for taking the time to put that together @glv :slight_smile: It’s a helpful visualization of what’s going on, and after some playing around I found that the fill’s opacity needs to be above 125ish for the get() command to return pure white, which gives me a better understanding of what’s going on.

It occurred to me to see if the issue happens in Photoshop as well, which it does. So this must be an inherent math thing rather than a processing thing. I think I’ll try experimenting with blend modes over opacity when I have time later. I have a feeling the solution’s in there somewhere.

1 Like

Well, if anybody else has trouble with this down the line, you can get rid of the ghost effect by using blendMode(MULTIPLY).

  blendMode(MULTIPLY);
  fill(0, 50);
  rect(0, 0 , width, height);
2 Likes

I wonder if it has to do with the type of screen being viewed on? I am on a mac…

Do you need to reset the blendMode to something else after drawing the rect over the screen? When I use blendMode(MULTIPLY) as shown, nothing else I draw in the program appears on the screen.

@philbutrin yes, you’ll need to change it back to default. blendMode(BLEND) will do it. You can also use the push/pop methods to revert back to previous settings

push()
blendMode(MULTIPLY);
fill(0, 10);
rect(0, 0, width, height);
pop();
1 Like

blendMode(ADD) will also work.

The reason that this happens with repeated use of blendMode(BLEND) is, in a nutshell, Zeno’s paradox. See prior discussion with example test sketches and some digging into the math here:

2 Likes

@jeremydouglass Thanks for taking the time to share the math, though I can’t say I understand it. I was under the impression that adding say, pure white with 5 alpha meant that you were adding 5/255 to the canvas until the whole thing became white. If you’re willing to to explain what’s actually going on here I’m super interested in hearing it, but if not I’m at least getting the effect I’m looking for even if I don’t understand what’s going on.

That would be true in blendMode(ADD). Then you add 5.
But with blendMode(BLEND) you get dest * (1 - srcAlpha) + src * (srcAlpha).

fill(0, 10) is 10/255 = ~0.039.

So it leaves 96% of a canvas pixel value each time it is applied.

96% of 13 is 12.46, which is saved as an int =12. (canvas pixels are ints). BUT:

96% of 12 is 11.52… which is 12. So a pixel that is at 12, when covered with fill(0, 10), will stay 12. And then 12. And then 12…

Watch it happen in this example sketch – it shrinks to 12, then stays there:

float dest = 255;           // white canvas pixel
int src = 0;                // black fill pixel
float srcAlpha = 10.0/255;  // black fill alpha channel

for(int i = 0; i<100; i++) {
  print(dest, " | ");
  float result = dest * (1 - srcAlpha) + src * (srcAlpha);  // blendMode(BLEND)
  dest = round(result);     // results saved as int in original pixel
}

12 is the specific resulting value for 10/255, but for different inputs you get different final values (towards white or black) where the final value gets stuck before reaching 0 (or 255).

2 Likes

Great. Thanks for the reply!

Great explanation, I get it now. Thanks for demystifying that for us :slight_smile:

You can control the pixel values directly with pixels[]:

void setup() {
  size(400, 400);
  
  stroke(#ffffff);
  strokeWeight(8);
}

void draw() {
  line(random(width), random(height), random(width), random(height));
  fade(4);
}

void fade(int amount) {
  loadPixels();
  for (int i = 0; i < pixels.length; i++) {
    int argb = pixels[i];
    int a = (argb >> 24) & 0xFF;
    int r = (argb >> 16) & 0xFF;
    int g = (argb >> 8) & 0xFF;
    int b = argb & 0xFF;
    r = max(0, r - amount);
    g = max(0, g - amount);
    b = max(0, b - amount);
    pixels[i] = (a << 24) | (r << 16) | (g << 8) | b;
  }
  updatePixels();
}
2 Likes