Cairo Linear Patterns and Opacity.

Subtitle - 9 Ways to Skin a Cat.

This is a follow-on from my 101 - 108 sequence of articles intended to help beginners to write GUI applications in the D programming language using the Gtkd-2 library that wraps GTK+3. For further backgound see the first article. The project files for this article are here.

As part of my development work on COMPO version 2, I was looking into the Cairo Patterns provided by Pattern.createLinear and Pattern.createRadial, and I thought I would write up some notes based on my findings on the former.

If you build and run the application, you'll see as it comes up what is just about the simplest demo of Pattern.createLinear. The program draws a heavy squiggle across its drawing area, and then masks it with an opacity gradient.

I arrived at the point by a circuitous route. My initial reading of the documentation for Pattern.addColorStopRgba() had left me with the impression that this method was the way to create more or less smooth piecewise gradients. My first use of it was to create an array of 50 opacity values with linearly inreasing values, and add stops using those values. That worked fine. But then much later I looked at the documentation again and re-read the sentence that says "This can be useful for reliably making sharp color transitions instead of the typical blend."

At that point I tried the rather obvious subterfuge of simply setting two stops - one at offset 0, with full opacity, and one at offset 1, with zero opacity. To my surprise, I got what you see above. This is visually indistinguishable from the result I'd got previously with the 50 linear stops.

I didn't like either of them very much. Subjectively, the heavily masked region at the left end was too short to do anything useful with (like for instance placing a red star ;=)). So I thought I'd experiment with ways to make the fade more evenly distributed.

Functions Tidily Constrained Between Zero and One

Well, linear fits in here well enough, but I'd already rejected that. So what functions are there that produce curves that pass through one - full opacity, and progress to, or through zero - full transparency. Well, millions probably, but the ones I came up with reasonably quickly are the power functions xn, a cosine curve, and a Bezier curve (there's your millions.)

In the present context, power functions are usually called gamma correction, and since this looked distinctly like a gamma correction problem, I tried that first. Here's what my method to create the array of opacities for the stops looked like at that point. It includes the GDK default, and my previous linear attempt:


   void setupStops()
   {
      double delta = 1.0/nStops;

      void gtkDefault()
      {
         opStops[0] = double.init;
      }

      void linear()
      {
         double op = 1;
         for (int i = 0; i < nStops; i++)
         {
            opStops[i] = op;
            op -= delta;
         }
      }

      // The power function 
      void gamma(double g)
      {
         double x = 0;
         for (int i = 0; i < nStops; i++)
         {
            opStops[i] = 1-pow(x, g);
            x += delta;
         }
      }

      switch (gType)
      {
         case 1:
            linear();
            break;
         case 2:
            gamma(1.5);
            break;
         case 3:
            gamma(2.2);
            break;
         case 4:
            gamma(3.0);
            break;
         default:
            gtkDefault();
            break;
      }
   }

Gamma 2.2 seems to do the job pretty well, as is usually the case, but 1.5 and 3 will have their uses. You'll see that I've also added a plot() method to the presentation that displays the opacity values as a red curve over the mask.

Not to be deterred, I proceeded to cosine, and added two variations. A simple cosine90() function that just takes opacity vlaues from the first quarter cycle, and a half cycle function cosine180() that shifts values in -1..1 into the range 0..2, and then divides those by two.


      void cosine90()
      {
         double a = 0;
         double op;
         for (int i=0; i < nStops; i++)
         {
            op = cos(a);
            opStops[i] = op;
            a += PI/2/nStops;
         }
      }

      void cosine180()
      {
         double a = 0;
         double op;
         for (int i=0; i < nStops; i++)
         {
            op = cos(a);
            opStops[i] = (op+1)/2;
            a += PI/nStops;
         }
      }
Once again, usable fades - we are getting spoiled for choice. So the Bezier is the last of the 0..1 functions:

      Coord bezierPoint(double t, Coord p0, Coord p1, Coord p2)
      {
         Coord c;
         c.x = pow(1-t, 2) * p0.x + 2 * (1-t) * t * p1.x + pow(t, 2) * p2.x;
         c.y = pow(1-t, 2) * p0.y + 2 * (1-t) * t * p1.y + pow(t, 2) * p2.y;
         return c;
      }

      void quadbezier(double x, double y)
      {
         Coord start = Coord(0, 1), end = Coord(1, 0), control = Coord(x, y);
         double t = 0;
         for (int i = 0; i < nStops; i++)
         {
            Coord a = bezierPoint(t, start, control, end);
            opStops[i] = a.y;
            t += delta;
         }
      }
There's also the possibility of dropping the 'passes through zero' constraint. In that case, exponential functions become available, and just to prod that direction I added one:

      void exponential(double howClose)
      {
         double x = 0;
         double op;
         for (int i=0; i < nStops; i++)
         {
            op = exp(x);
            opStops[i] = op;
            x -= howClose;
         }
      }
The argument to the exponential function determines how close to full transparency the mask will get. It still starts at full opacity. This is not particularly impressive, and there are undoubtedly better alternatives when the 'passes through zero' condition is relaxed.

How Many Stops

I worked through this stuff with my original 50 stops, but this is excessive. If I reduce nStops to 10, everything except the exponential one looked much the same, and that can be fixed by a larger howClose value.

TBD

There's quite a bit more work to be done after this - you'll want to be able to fade the opposite way around, but you can use the same opStops array from end to beginning for that. Also you'll want to be able to fade in the vertical direction. That all turns out to be quite mechanical. Then there are all the experiments you can do when you specify colors other than white to go along with the opacity - Have fun!