10. Special Effects: A Fading Circle

In these notes you will learn:

  • How to make an object fade away by changing its transparency.
  • How to use the constrain function.
  • How to write you own version of the constrain function.

10.1. Introduction

So far, all of the animations we’ve created move an object by changing its position. In this note we’ll see another kind of animation: fading. When the user clicks on the screen, a big blue circle appears. It then begins to fade away until it is invisible.

10.2. Organizing our Demo Program

The first step in writing any program is to decide exactly what it should do. Since we are writing a demonstration of a new kind of animation, we want to keep it relatively simple. So let’s agree that the program works as follows: when the user clicks on the screen a circle appears at the click-point and then fades away to invisibility.

We’ll need variables to store the centre (x, y) of the ball. The circle won’t be moving or changing shape, so we don’t need dx, dy, or diam.

How do we get the circle to fade? Fading means that the circle starts out with some visible colour, and then at a certain rate, it becomes more transparent, until it disappears completely.

Recall that Processing lets you specify a colour’s alpha value, which sets the transparency of a colour. Alpha values in Processing range from 0 to 255, where 0 is totally transparent (i.e. invisible), and 255 is totally opaque.

So we will implement fading by slowly decreasing the alpha value of the circle’s fill colour.

10.3. First Draft: Just the Circle

We’ll start by only displaying the circle wherever the user clicks with the mouse. There is no fading yet; we’ll add that next.

float x;
float y;

void setup() {
    size(500, 500);
    smooth();
}

void draw() {
    background(255);
    noStroke();
    fill(0, 0, 255);
    ellipse(x, y, 150, 150);
}

void mousePressed() {
    x = mouseX;
    y = mouseY;
}

Recall that the mousePressed() function is called automatically by Processing whenever the user clicks a mouse button. When it’s called here, it sets x and y to be the position of the mouse, and so when draw() is called the circle will be drawn centred at the most recent mouse-click.

10.4. Adding Fading

Now let’s add the fading effect. We’ll need a variable to keep track of the current transparency:

float transparency;

Let’s agree that transparency ranges from 0 (totally transparent) to 1 (totally opaque). Why 0 to 1? Because it makes it easier to talk about percentages. e.g. 0.42 is exactly 42% of the way between 0 and 1. We say that we have normalized the transparency value.

We will also need a variable that tells us how much to decrease the transparency on each call to draw(). In other words, we need a variable that stores how fast the transparency changes:

float dtrans;

We call this variable dtrans following the convention of starting “rate of change” variables (such as dx and dy) with the letter dy.

Now we can add the fading effect. We need only make a few changes:

  • Whenever the user clicks the mouse, the circle is drawn at a new location with a total opaque transparency. Thus we need to set transparency to ` at the end of mouseClick():

    void mousePressed() {
        x = mouseX;
        y = mouseY;
        transparency = 1;
    }
    
  • Whenever the circle is drawn, its fill colour must be drawn taking the transparency into account. This is not completely straightforward. For instance, the following doesn’t work:

    void draw() {
        background(255);
        noStroke();
    
        fill(0, 0, 255, transparency); // WRONG!!
        ellipse(x, y, 150, 150);
    }
    

    The problem is that the fill function expects a value in the range 0 to 255 (which we’ve already seen in the fill() function documentation). But transparency ranges from 0 to 1.

    We could of course change our previous decision and let transparency range from 0 to 255. That would be fine for this one program. But in general, it’s probably not a good idea. In large programs, you may have several different range, and keeping track of all of them can be hard if not downright impossible. This another advantage (and the origin of the name) of normalizing the values. It’s simpler to require that all ranges start and end at 1.

    With that in mind, how can we solve our problem? Recall that Processing offers the map() function to convert one range to another:

    void draw() {
        background(255);
        noStroke();
    
        float newTrans = map(transparency, 0, 1, 0, 255);
        fill(0, 0, 255, newTrans);
        ellipse(x, y, 150, 150);
    }
    
  • After the circle is drawn, we need to change transparency by adding dtrans to it:

    void draw() {
        background(255);
        noStroke();
    
        float newTrans = map(transparency, 0, 1, 0, 255);
        fill(0, 0, 255, newTrans);
    
        ellipse(x, y, 150, 150);
    
        transparency += dtrans;
    }
    

    Since we’ve decide to add dtrans to transparency (instead of subtracting it), we must remember to initialize dtrans to a negative value, in order to get a fading effect.

    This gives rise to an important consideration. We’ve decided that transparency ranges from 0 to 1, but the statement transparency += dtrans does not respect that range. In other words, assuming that dtrans is negative means that transparency will become negative at some point.

    Sometimes this might not matter. In fact, in this program it doesn’t matter because it turns out that if you call fill with a negative alpha value then the it is treated as if it were a 0.

    In general however, it is unwise to rely on a function whose workings we don’t know to deal with our errors gracefully. It’s quite possible that the equivalent of fill in some other language will respond by terminating your program. What we need is a way to constrain the value of transparency to the range 0 to 1. Obligingly, Processing provides with just the tool, going by the appropriate name constrain:

    void draw() {
        background(255);
        noStoke();
    
        float newTrans = map(transparency, 0, 1, 0, 255);
        fill(0, 0, 255, newTrans);
        ellipse(x, y, 150, 150);
    
        transparency += dtrans;
        transparency = constrain(transparency, 0, 1);
    }
    

    The function constrain(x, start, end) always returns a value between start and end. If x happens to be between start and end, then x is returned. Otherwise, if x is less than start, then start is returned, and if x is greater than end than end is returned. In our case this corresponds to either transparency, 0, or 1 being returned, respectively.

    In fact, it’s not that hard to write your own version of constrain, once we are familiar with one more form of if-statement: the if-else-if:

    float myConstrain(float x, float start, float end) {
        if (x < lo) {
            return lo;
        } else if (x > hi) {
            return hi;
        } else {
            return x;
        }
    }
    

Sometimes (some might say often) the check that we wish to perform doesn’t fall neatly into one of the two states: true or false. In the case of constrain, there are three possible outcomes: Either x < start, or x > end, or start <= x && x <= end. We can describe boolean expressions with as many outcomes as we wish using the if-else-if code block. Here it is in general form:

if (cond1) {
    //... statements to execute if cond1 is true .../
} else if (cond2) {
    //... statements to execute if cond2 is true .../
} else if (cond3) {
    //... statements to execute if cond2 is true .../
}
.
.
.
} else if (condN) {
    //... statements to execute if cond2 is true .../
} else {
    // This last statement is optional.
}

Note that we could have written myConstrain without the last else clause:

float myConstrain(float x, float start, float end) {
    if (x < start) {
        return start;
    } else if (x > end) {
        return end;
    }
    return x;
}

10.5. Questions

  1. What are alpha values?
  2. What is the range of alpha values in a Processing color function such as fill?
  3. When is mousePressed() called?
  4. Why does the variable transparency range from 0 to 1?
  5. Why is dtrans set to a negative value in the fading circle program?
  6. What does the constrain function do?

10.6. Programming Questions

  1. Add a background image to the fading circle program. As the circle fades away, the underlying image should become clearer.
  2. Modify the fading circle program so the circle fades in (instead of fades out). When the user clicks with the mouse, the circle begins invisible and then slowly becomes fully opaque.
  3. Modify the fading circle program so that after the transparency of the circle fades to 0, it starts to fade back in, and then it fades back out, and so on forever. The circle should appear to slowly pulse.
  4. Read about the tint() function and then modify the fading circle program to make an arbitrary image (loaded from a file) fade away.

10.7. The Fading Circle Program

float x;
float y;

float transparency; // 0 <= transparency <= 1
float dtrans;       // the change in transparency on each call to draw

void setup() {
  size(500, 500);
  dtrans = -0.01;
  smooth();
}

void draw() {
  background(255);
  noStroke();
  float a = map(transparency, 0, 1, 0, 255);
  fill(0, 0, 255, a);
  ellipse(x, y, 150, 150);
  transparency += dtrans;
  transparency = constrain(transparency, 0, 1);
}

void mousePressed() {
  x = mouseX;
  y = mouseY;
  transparency = 1;
}