Click here to Skip to main content
15,884,425 members
Articles / Web Development / HTML

Simple radial gradient implementation with XAML sample application for X11 (but without the need for any external library)

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
26 Sep 2016CPOL11 min read 23.4K   198   2   10
How to calculate a radial gradient and draw the output on a System.Drawing.Bitmap. And how to use this bitmap as a tile pixmap to fill any path.

Image 1 Download XamlAlienSokoban_RadialGradient_X11_32.zip Mono project including full source and executable

Image 2 Download XamlAlienSokoban_RadialGradient2_X11_32.zip (second approach) Mono project including full source and executable

Introduction

I was looking for a radial gradient algorithm implementation, i can use without integration of any external library (especially Cairo) for my Roma Widget Set (C# X11) project. (The result i'll introduce is generic and can be used for Win32 and Windows Forms as well.)

It was more a challenge to solve the problem somehow, than an approach to be better than any professional radial gradient implementation - because i examine the challenge from a programmer's point of view, not from a mathematician's point of view.

There are two interpretations of a radial gradient:

  1. Radial gradients, that produce something looking like/interpretable as a 2D top-view projection of a 3D cone and require an ellipse/cone base (centerX, centerY, radiusX, radiusY) as well as a focal point/apex (originX, originY). See the OpenVG Specification for basics. Implemented, for instance, by WPF.

    Supplement: I've been unhappy with the performance of my initial approach. You can skip the chapter 'Background (initial approach)' later on, if you want to read abou the final solution only.

    The image shows a sample with radiusX != radiusY and [centerX, centerY] != [originX, originY].
    Image 3
    The color gradient stop definitions, used for the image, are:
    <GradientStop Color="#FF000000" Offset="0"/>
    <GradientStop Color="#FF000000" Offset="0.177966"/>
    <GradientStop Color="#FFFFFFFF" Offset="0.199153"/>
    <GradientStop Color="#FEFFFFFF" Offset="0.25"/>
    <GradientStop Color="#FEFF0000" Offset="0.275424"/>
    <GradientStop Color="#FFFF0000" Offset="0.34322"/>
    <GradientStop Color="#FFFFFFFF" Offset="0.36017"/>
    <GradientStop Color="#FFFFFFFF" Offset="0.677966"/>
    <GradientStop Color="#FF838383" Offset="1"/>
  2. Radial gradients, that produce something looking like/interpretable as a 2D projection of a 3D tube and require a start circle/tube start (startX, startY, startRadius) as well as an end circle/tube end (endX, endY, endRadius). Implemented, for instance, by the HTML5 canvas and Cairo. If end circle is completely enclosing the start circle and start circle radius is near 0.0, the second interpretations of a radial gradient creates results similar to the first one (left image). Otherwise it creates a clearly recognizable tube projection (right image).

    Supplement: I've found a very old and stable (because it's used by the X11 extension Xrender and Cairo) as well as very well documented C source, implementing this approach, in pixman's file pixman-radial-gradient.c. It has also been mercilessly copied to Chrome/Android/Mozilla's shader library 'skia' file SkGradientShader.cpp.

    The left image shows a sample with the start circle completely enclosed in the end circle.
    The right image shows a sample with the start circle completely outside (right top) the end circle.
    Image 4                   Image 5
    The color gradient stop definitions, used for the images are:
                                                  radgrad.addColorStop(0.000000, '#FFFFFF');
    radgrad.addColorStop(0.000000, '#000000');    radgrad.addColorStop(0.010000, '#000000');
    radgrad.addColorStop(0.177966, '#000000');    radgrad.addColorStop(0.177966, '#000000');
    radgrad.addColorStop(0.199153, '#FFFFFF');    radgrad.addColorStop(0.199153, '#FFFFFF');
    radgrad.addColorStop(0.250000, '#FFFFFF');    radgrad.addColorStop(0.250000, '#FFFFFF');
    radgrad.addColorStop(0.275424, '#FF0000');    radgrad.addColorStop(0.275424, '#FF0000');
    radgrad.addColorStop(0.343220, '#FF0000');    radgrad.addColorStop(0.343220, '#FF0000');
    radgrad.addColorStop(0.360170, '#FFFFFF');    radgrad.addColorStop(0.360170, '#FFFFFF');
    radgrad.addColorStop(0.677966, '#FFFFFF');    radgrad.addColorStop(0.677966, '#FFFFFF');
    radgrad.addColorStop(1.000000, '#838383');    radgrad.addColorStop(0.990000, '#838383');
                                                  radgrad.addColorStop(1.000000, '#FFFFFF');

The radial gradient algorithm implementation i want to introduce is regarded to the first approach.

Background (initial approach)

I've found two articles on the web, that discuss the theoretical basics:

You are welcome to give feedback, if you know additional references!

Some basics

The color of a pixel is calculated on the base of the color gradient stop definitions, that define color values within an offset range from 0.0 to 1.0. The gradient stop offset can be interpreted as distance dP from the focal point/apex (originX, originY) to the pixel - normalized by the distance dE from the focal point/apex (originX, originY) to the ellipse boundary.

Image 6

Assumed the distance dP is  √ (ΔxP² + ΔyP²) = √ (15² + 20²) = 25 and the distance dE is √ (ΔxE² + ΔyE²) = √ (43.5² + 58²) = 72.5 - the normalized distance dN is  dP/ dE = 25/72.5 = 0.3448.

Assumed the relevant color gradient stop definitions are

...
<GradientStop Color="#FFFF0000" Offset="0.34322"/> <!-- Stop5 -->
<GradientStop Color="#FFFFFFFF" Offset="0.36017"/> <!-- Stop6 -->
...

the pixel color cP[aarrggbb] is calculated as (1.0 - dN) * Stop5[aarrggbb] +  dN * Stop6[aarrggbb].

Thus cP[aarrggbb] = (1.0 - 0.3448) * Stop5[FFFF0000] + 0.3448 * Stop6[FFFFFFFF] =  [58580000] + [A7A7A7A7] = [FFFFA7A7].

Calculation strategy

Image 7

FIRST The implemented radial gradient algorithm loops top down through the scan lines of the bitmap (0 <= y < bitmap.Height).

SECOND For every scan line above the centerY coordinate an ellipse center originated angle sweeps the range from 1.5 * PI to -0.5 * PI. For every scan line below the centerY coordinate an ellipse center originated angle sweeps the range from 0.5 * PI to 2.5 * PI.

This is done to

  • ensure that all pixel of the scan line are calculated and the
  • ellipse boundary point pE can be calculated easily (boundaryX² / radiusX² + boundaryY² / radiusY² = 1).

THIRD The ellipse point pE coordinates are translated into pE(F) coordinates relative to the focal point/apex pF. Since the scan line's y coordinate is given, the scan line's x coordinate of pixel pS can be calculated baset on pF easyly with this equiation: pS.X = pE(F).X / |pE(F).Y| * |pS.Y| A scan line x coordinate outside the bitmap boundaries is discarded.

FOURTH The pixel color is calculated as already described and set to the pixel.

Calculation problems

• The first problem is speed. Currently the top down loop through the scan lines of the bitmap is implemented with a single thread. Instead, since there is no retroactive effect between the scan lines, every scan line could be calculated seperately (multi threaded). The only challenge is the competting bitmap access, that could be solved with a managed pixel array buffer and a final transfer from managed pixel array buffer to bitmap bits.

// Loop through the scan lines.
for (int y = 0; y < bitmap.Height; y++)
...

// Could be optimized to:
Parallel.For(...)
...

• The second problem is accuracy. The algorithm is not accurate for center originated angles near the values PI and 0.0 (or 2 * PI). The angel values of PI and 0.0 aren't calculatable at all (division by zero within THIRD step). Instead the angles near the values PI and 0.0 could be calculated scan column based instead scan line based, but this requires the implementation of a second calculation strategy. Currently the accuracy is tuned to be sufficient, but this results in many duplicate calculations for the same pixel. The scan line at the angles PI and 0.0 is interpolated by the scan lines directly above and below.

// Catch up the skiped line.
if (absoluteOrigin.Y >= 0 && absoluteOrigin.Y < bitmap.Height)
{
    if      (absoluteOrigin.Y > 0 && absoluteOrigin.Y < bitmap.Height - 1)
    {
        for (int x = 0; x < bitmap.Width; x++)
        {
            System.Drawing.Color c1 = bitmap.GetPixel (x, absoluteOrigin.Y - 1);
            System.Drawing.Color c2 = bitmap.GetPixel (x, absoluteOrigin.Y + 1);
            System.Drawing.Color c  = System.Drawing.Color.FromArgb(
                                          Math.Min(255, (int)(c1.R * 0.5d + c2.R * 0.5d + 0.49d)),
                                          Math.Min(255, (int)(c1.G * 0.5d + c2.G * 0.5d + 0.49d)),
                                          Math.Min(255, (int)(c1.B * 0.5d + c2.B * 0.5d + 0.49d)));
            bitmap.SetPixel (x, absoluteOrigin.Y, c);
        }
    }
    else if (bitmap.Height > 1 && absoluteOrigin.Y == 0)
    {
        for (int x = 0 + 1; x < bitmap.Width; x++)
        {
            System.Drawing.Color c = bitmap.GetPixel (x, absoluteOrigin.Y + 1);
            bitmap.SetPixel (x, absoluteOrigin.Y, c);
        }
    }
    else if (bitmap.Height > 1 && absoluteOrigin.Y == bitmap.Height - 1)
    {
        for (int x = 0 + 1; x < bitmap.Width; x++)
        {
            System.Drawing.Color c = bitmap.GetPixel (x, absoluteOrigin.Y - 1);
            bitmap.SetPixel (x, absoluteOrigin.Y, c);
        }
    }
}

• The third problem is robustness. The algorithm is not robust in cases where the focal point/apex is outside the ellipse boundary and particularly above and right the center as well as below and left the center. This is because in this two cases the scan line pixel have two center originated angles, that calculate one pixel's color.

Image 8

In these cases the ellipse center originated angle must swap the start and end angles of the range to sweep in order to calculate useful results.

bool    loopScanlineRtL                = (absoluteOrigin.X <= absoluteCenter.X ? false : true);
bool    loopCenteredEllipseAngleRtL    = (loopScanlineRtL ? pixelAboveOrigin : !pixelAboveOrigin);

...

// Swap the start and end angles of the range to sweep.
if (loopScanlineRtL)
{
    double buffer = firstCenteredEllipseAngle;
    firstCenteredEllipseAngle = lastCenteredEllipseAngle;
    lastCenteredEllipseAngle = buffer;
}

// Loop through the centered ellipse angle range to calculate the scanline's pixel.
for (double centeredEllipseAngle = firstCenteredEllipseAngle;
     (loopCenteredEllipseAngleRtL ? centeredEllipseAngle <= lastCenteredEllipseAngle
                                  : centeredEllipseAngle >= lastCenteredEllipseAngle);
     centeredEllipseAngle += (loopCenteredEllipseAngleRtL ? angleIncrement : -angleIncrement))
{
    ...
}

• The fourth problem are gaps. Sometimes, esecially near the angel values of PI and 0.0 (or 2 * PI), the resolution of the trigonometric functions is insufficient to calculate at least one color value for every pixel. To prevent gaps (typically emerging at the very begin or end of the scan line), the first calculated color will be propagated to all previous pixel and the last calculated color will be propagated up to the boundary of the bitmap.

// Fill skipped/missing pixel at the start of this scan line with current color.
if (!loopScanlineRtL)
{
    // Typically the scanline's x-coordinate is incremented.
    if (lastX + 1 < x)
    {
        for (int subX = lastX + 1; subX < x; subX++)
        {
            bitmap.SetPixel (subX, y, c);
            countPixelSettings++;
        }
    }
}
else
{
    // Typically the scanline's x-coordinate is decremented.
    if (lastX - 1 > x)
    {
        for (int subX = lastX - 1; subX > x; subX--)
        {
            bitmap.SetPixel (subX, y, c);
            countPixelSettings++;
        }
    }
}
lastX = x;

// Propagate current color to the end of the scan line to prervent skipped/missing pixel.
if (!loopScanlineRtL)
{
    if (x + 5 >= bitmap.Width)
    {
        for (int subX = x + 1; subX < bitmap.Width; subX++)
        {
            bitmap.SetPixel (subX, y, c);
            countPixelSettings++;
        }
    }
}
else
{
    if (x - 5 <= 0)
    {
        for (int subX = x - 1; subX >= 0; subX--)
        {
            bitmap.SetPixel (subX, y, c);
            countPixelSettings++;
        }
    }
}

With all the introduced extra effort the implemented radial gradient algorithm calculates useful results for special cases as well. The next image shows four of the special cases (with the focal point/apex outside the ellipse boundary). The results looks identical to that, what WPF would produce with the same start values.

Image 9

• The last problem is a pitfall concerning the size of the gradient bitmap. The gradient bitmap must be large enough to include the complete outline of the figure to fill with a radial gradient under any circumstances. Otherwise unexpected access violation (SIGSEGV) errors orrure partly late after the problem has been triggered (i assume they occure at that time, the tile pixmap with the radial gradient is applied to be the figure's fill during the XFillRectangle(), XFillPolygon() call).

Currently the gradient bitmap is enlarged by 8px and alignet to a multiple of 8px to avoid these access violation errors.

System.Drawing.Size   minBmpSize = new System.Drawing.Size   (
    (int)(_bounds.Width + 0.49d), (int)(_bounds.Height + 0.49d));
// Enlarge the bitmap by a minimum of 8px and align the size to a multiple of 8.
System.Drawing.Bitmap bitmap     = new System.Drawing.Bitmap (
    (minBmpSize.Width  % 8 == 0 ? minBmpSize.Width  + 8 : minBmpSize.Width  + 16 - minBmpSize.Width  % 8),
    (minBmpSize.Height % 8 == 0 ? minBmpSize.Height + 8 : minBmpSize.Height + 16 - minBmpSize.Height % 8),
    pixelFormat);

...

// Convert standardized coordinates to pixel coordinates.
System.Drawing.Point absoluteCenter = new System.Drawing.Point (
    (int)(_center.X * bitmap.Width  + 0.49d),         (int)(_center.Y * bitmap.Height + 0.49d));
System.Drawing.Point absoluteRadius = new System.Drawing.Point (
    (int)(_radiusX  * minBmpSize.Width  + 0.49d),     (int)(_radiusY  * minBmpSize.Height + 0.49d));
System.Drawing.Point absoluteOrigin = new System.Drawing.Point (
    (int)(_gradientOrigin.X * bitmap.Width  + 0.49d), (int)(_gradientOrigin.Y * bitmap.Height + 0.49d));

But this approach requires an adjustment of the gradient bitmap by half of the enlargement (excentric).

System.Windows.Rect bounds     = ((X11RadialGradientBrushInfo)fill).Bounds;
System.Drawing.Size prfBmpSize = X11GradientBrushInfo.PreferredBitmapSize (bounds.Size);
System.Drawing.Size excentric  = new System.Drawing.Size (
                                         (prfBmpSize.Width  - (int)(bounds.Width  + 0.49)) / 2,
                                         (prfBmpSize.Height - (int)(bounds.Height + 0.49)) / 2);
                                
X11.X11lib.XSetTSOrigin  (this.Display, x11gc,
                          (X11.TInt)(bounds.Left + 0.49) - excentric.Width,
                          (X11.TInt)(bounds.Top  + 0.49) - excentric.Height);

Background (second approach)

Performance issues

I've been unhappy with the performance of my initial approach (primal code). I've reworked the data structure and prepared multi threading to calculate each scan line by a separate thread (reworked code). The measured values for a 208 x 120px bitmap on a two core virtual machine are as follows:

          primal code             reworked code
1. value: 1990610 ticks / 199 ms  1652958 ticks / 165 ms
2. value: 2027815 ticks / 202 ms  1743948 ticks / 174 ms
3. value: 2032046 ticks / 203 ms  1655123 ticks / 165 ms
4. value: 2239863 ticks / 223 ms  1662558 ticks / 166 ms
5. value: 2182778 ticks / 218 ms  1686376 ticks / 168 ms

I switched over to multi threading and had high hopes to be much faster - but:

          1 thread                2 threads               8 threads               50 threads
1. value: 2181228 ticks / 218 ms  7532660 ticks / 753 ms  3822407 ticks / 382 ms  1936374 ticks / 193 ms
2. value: 2428252 ticks / 242 ms  6955946 ticks / 695 ms  3057482 ticks / 305 ms  1723857 ticks / 172 ms
3. value: 2223063 ticks / 222 ms  7729761 ticks / 772 ms  2812697 ticks / 281 ms  2317020 ticks / 231 ms
4. value: 2752272 ticks / 275 ms  7622548 ticks / 762 ms  2934314 ticks / 293 ms  1728051 ticks / 172 ms
5. value: 2595338 ticks / 259 ms  5952311 ticks / 595 ms  3589991 ticks / 358 ms  1715790 ticks / 171 ms

Obviously multi threading creates an overhead greater than the saving, even 50 threads aren't batter than the single threaded reworked code. That was very disappointing.

Thus i wanted to check out how good my initial approach is, compared to the most simple way of drawing radial gradients: Drawing ellipses.

Calculation Stragegy

The most simple way of drawing radial gradients is to draw concentric ellipse outlines with uniform colors. Such algorithm just has to make sure, that every bitmap pixel has been set once at least. The most effective algorithm would calculate the ellipses in a way, that every bitmap pixel has been set exactly once - but this goal is very hard to achieve.

The next image shows the principle of this approach.

Image 10

I've choosen a pixel overlapping of 1.5 (33%) at the largest radius r (where r = √ (rX² + rY²) for the ellipse formula pX²/rX² + pY²/rY² = 1). Since my implementation shall support focal point/apex position outside the ellipse boundary, it has to consider the outside distance as well:

C#
// Consider a focal point/apex position outside the ellipse boundary as well.
double maxRadiusShiftX = absoluteRadius.Width;
if (absoluteOrigin.X < absoluteCenter.X - absoluteRadius.Width)
    maxRadiusShiftX += (absoluteCenter.X - absoluteRadius.Width) - absoluteOrigin.X;
if (absoluteOrigin.X > absoluteCenter.X + absoluteRadius.Width)
    maxRadiusShiftX += absoluteOrigin.X - (absoluteCenter.X + absoluteRadius.Width);
double maxRadiusShiftY = absoluteRadius.Height;
if (absoluteOrigin.Y < absoluteCenter.Y - absoluteRadius.Height)
    maxRadiusShiftY += (absoluteCenter.Y - absoluteRadius.Height) - absoluteOrigin.Y;
if (absoluteOrigin.Y > absoluteCenter.Y + absoluteRadius.Height)
    maxRadiusShiftY += absoluteOrigin.Y - (absoluteCenter.Y + absoluteRadius.Height);

// Multiply with 1.50 to realize 33% pixel overlapping at the most problematic angle.
int radiusSteps = (int)(Math.Sqrt(maxRadiusShiftX * maxRadiusShiftX +
                                  maxRadiusShiftY * maxRadiusShiftY) * 1.50 + 0.49);

If radial gradient focal point/apex position is not equal the radial gradient center position, every ellipse outline to draw has a different ellipse center on a straight from the radial gradient center position to the radial gradient focal point/apex position.

C#
// Loop through the ellipses.
double distCenterToOriginX = absoluteCenter.X - absoluteOrigin.X;
double distCenterToOriginY = absoluteCenter.Y - absoluteOrigin.Y;

double normalizedProgress  = 1.0d;

System.Windows.Point currR = new System.Windows.Point (absoluteRadius.Width, absoluteRadius.Height);
System.Windows.Point currC = new System.Windows.Point (absoluteCenter.X, absoluteCenter.Y);
while (normalizedProgress > 0.001d)
{
    DrawEllipse(currR, currC, colorStops, Math2.Clamp(normalizedProgress, 0.0d, 1.0d), bitmap);
    currR.X -= absoluteRadius.Width  * 1.0d / radiusSteps;
    currR.Y -= absoluteRadius.Height * 1.0d / radiusSteps;
    currC.X -= distCenterToOriginX / radiusSteps;
    currC.Y -= distCenterToOriginY / radiusSteps;
    normalizedProgress -= 1.0d / radiusSteps;
}

A larger ellipse intersects more bitmap points than a smaller ellipse. I try to take this into account:

C#
// Try to calculate the greatest (fastest) possible angle step which still
// procuces the highest quality image.
double radiusLen = Math.Sqrt (currentRadius.X * currentRadius.X + currentRadius.Y * currentRadius.Y);
double alphaStep = 0.0016;
if      (radiusLen < 15.0)    alphaStep *= 20.0;
else if (radiusLen < 30.0)    alphaStep *= 14.0;
else if (radiusLen < 45.0)    alphaStep *= 10.0;
else if (radiusLen < 60.0)    alphaStep *= 8.0;
else if (radiusLen < 75.0)    alphaStep *= 7.0;
else if (radiusLen < 90.0)    alphaStep *= 6.0;
else if (radiusLen < 105.0)    alphaStep *= 5.0;
else if (radiusLen < 120.0)    alphaStep *= 4.0;
else if (radiusLen < 150.0)    alphaStep *= 3.5;
else if (radiusLen < 180.0)    alphaStep *= 3.0;
else if (radiusLen < 210.0)    alphaStep *= 2.5;
else if (radiusLen < 240.0)    alphaStep *= 2.0;
else if (radiusLen < 300.0)    alphaStep *= 1.5;

double Pi2 = Math.PI * 2;
for (double alpha = 0.0d; alpha < Pi2; alpha += alphaStep)
{
    ...
}

The performance i get from this algotithm is nearly 10 times better than from the initial approach:

          second approach
1. value: 164672 ticks / 16 ms
2. value: 166576 ticks / 16 ms
3. value: 171895 ticks / 17 ms
4. value: 160523 ticks / 16 ms
5. value: 174914 ticks / 17 ms

Based on the recent experience i waive to switch to multi threading.

Using the code

All the radial gradient algorithm code (the initial approach as well as the second approach) is contained in the class X11RadialGradientBrushInfo method TilePixmap(...) of my Roma Widget Set (C# X11) project starting with version Image 11. To test the implementation and show the fitness of the algorithm, a sample application is provided with this article.

The sample application is a MVVM (Model View ViewModel) design pattern based X11 application with UI definition via XAML using the Roma Widget Set (Xrw). It is a zero dependency GUI application framework for X11 (it requires only assemblies of the free Mono standard installation and libraries of the free X11 distribution; it doesn't particularly require GNOME, KDE or commercial libraries) and is implemented entirely in C#. The article Writing a XAML dialog application for X11 describes the basics of Xrw's XAML wrapper.

Since the radial gradient algorithm implementation is based on the class System.Drawing.Bitmap, a fransfer to other platforms than X11, that support the System.Drawing.Bitmap class, is easy.

The sample application is based on the idea and background image, taken from the woderful WPF Alien Sokoban by Daniel Vaughan. The sample application was written with Mono Develop 2.4.1 for Mono 2.8.1 on OPEN SUSE 11.3 Linux 32 bit EN and GNOME desktop. Neither the port to any older nor to any newer version should be a problem. The sample application's solution consists of four projects (the complete sources are provided for download):

  • XamlAlienSokoban contains the source code of the sample application.
  • XamlPreprocessor contains the source code of the XAML preprocessor.
  • Xrw contains the Roma Witget Set (Xrw) and (introduced with Image 12) the HTML documentation of the API
  • X11Wrapper defines the function prototypes, structures and types for Xlib/X11 calls to the libX11.so

All projects contain the complete source code. The Projects Xrw and X11Wrapper represent a preview of the Roma Widget Set prospective version Image 13.

The next image shows the sample application on OPEN SUSE 11.3 Linux 32 bit EN and GNOME desktop.

Image 14

The image contains four aliens, each of them entirely composed by XAML code. Body (ellipse) and legs (path) show a radial gradient from light green to green. The eye (ellipse) shows a radial gradient from black via white, red and white to gray.

This is the XAML code of the 'front' alien (Alien1).

<Path x:Name="Alien1_LeftLeg" Width="109" Height="193" Canvas.Left="300" Canvas.Top="650" Stretch="Fill"
      Data="F1 M 84,58 C 84,59 11,116 80,158 C 149,199 33,190 29,192 C 25,195 26,206 28,208 C 32,211 66,209 66,209 C 42,220 42,220 40,222 C 38,226 45,233 50,234 C 54,233 53,231 74,219 C 66,237 65,236 65,240 C 65,243 77,247 79,245 L 91,227    C 129,174 129,179 129,174 C 128,165 80,124 80,123 C 80,122 113,88 113,88 L 84,59 z" StrokeThickness="0" StrokeLineJoin="Round">
<!--           M       C knee (c1 ip c2)     C toe3 (c1 ip c2)       C toe3 (c1 ip c2)      C toe-stem (c1 ip c2)  C toe2 (c1 ip c2)      C toe2 (c1 ip c2)      C toe-stem (c1 ip c2)  C toe1 (c1 ip c2)      C toe1 (c1 ip c2)      L toe-stem  C heel                    C  hollow of the knee   C thigh                L thigh -->
    <Path.Fill>
        <RadialGradientBrush RadiusX="0.812102" RadiusY="0.444431" Center="0.427898,0.539459" GradientOrigin="0.427898,0.539459">
            <RadialGradientBrush.GradientStops>
            <GradientStop Color="#FF00FF00" Offset="0"/>
            <GradientStop Color="#FF1F9E03" Offset="1"/>
            </RadialGradientBrush.GradientStops>
        </RadialGradientBrush>
    </Path.Fill>
</Path>
<Path x:Name="Alien1_RightLeg" Width="109" Height="193" Canvas.Left="530" Canvas.Top="650" Stretch="Fill"
      Data="F1 M 76,58  C 76,58 145,112 76,153 C 18,187 123,183 128,185 C 132,187 132,201 127,202 C 123,202 118,200 89,199 C 111,211 114,212 117,216 C 120,220 116,227 112,228 C 109,229 100,222 79,209 C 92,232 89,232 89,235 C 88,239 76,240 74,238 L 59,198  C 30,164 26,172 26,163 C 27,154 73,117 73,116 C 73,115 45,84 45,84 L 76,58 z" StrokeThickness="0" StrokeLineJoin="Round">
<!--           M        C knee (c1 ip c2)      C toe3 (c1 ip c2)        C toe3 (c1 ip c2)         C toe-stem (c1 ip c2)    C toe2 (c1 ip c2)         C toe2 (c1 ip c2)         C toe-stem (c1 ip c2)    C toe1 (c1 ip c2)      C toe1 (c1 ip c2)      L toe-stem C heel                C  hollow of the knee  C thigh              L thigh -->
    <Path.Fill>
        <RadialGradientBrush RadiusX="0.812102" RadiusY="0.444431" Center="0.427898,0.539459" GradientOrigin="0.427898,0.539459">
            <RadialGradientBrush.GradientStops>
            <GradientStop Color="#FF00FF00" Offset="0"/>
            <GradientStop Color="#FF1F9E03" Offset="1"/>
            </RadialGradientBrush.GradientStops>
        </RadialGradientBrush>
    </Path.Fill>
</Path>
<Ellipse x:Name="Alien1_Body" Width="293" Height="320" Canvas.Left="324" Canvas.Top="386">
    <Ellipse.Fill>
        <RadialGradientBrush RadiusX="0.5" RadiusY="0.5" Center="0.5,0.5" GradientOrigin="0.5,0.5">
            <GradientStop Color="#FF00FF00" Offset="0.00847458"/>
            <GradientStop Color="#FF14D800" Offset="0.728814"/>
            <GradientStop Color="#FF0D8301" Offset="1"/>
        </RadialGradientBrush>
    </Ellipse.Fill>
</Ellipse>
<Ellipse x:Name="Alien1_Eye" Width="150" Height="155" Canvas.Left="400" Canvas.Top="413">
    <Ellipse.Fill>
        <RadialGradientBrush RadiusX="0.5" RadiusY="0.5" Center="0.5,0.5" GradientOrigin="0.5,0.5">
            <GradientStop Color="#FF000000" Offset="0"/>
            <GradientStop Color="#FF000000" Offset="0.177966"/>
            <GradientStop Color="#FFFFFFFF" Offset="0.199153"/>
            <GradientStop Color="#FEFFFFFF" Offset="0.25"/>
            <GradientStop Color="#FEFF0000" Offset="0.275424"/>
            <GradientStop Color="#FFFF0000" Offset="0.34322"/>
            <GradientStop Color="#FFFFFFFF" Offset="0.36017"/>
            <GradientStop Color="#FFFFFFFF" Offset="0.677966"/>
            <GradientStop Color="#FF838383" Offset="1"/>
        </RadialGradientBrush>
    </Ellipse.Fill>
</Ellipse>
<Path x:Name="Alien1_Mouth" Width="155" Height="65.5" Canvas.Left="389" Canvas.Top="586" Stretch="Fill" StrokeThickness="9" StrokeLineJoin="Round" Stroke="#FF000000" Fill="#FF000000" Data="F1 M 591,301 C 631,303 643,317 675,315 C 708,314 737,292 737,292 C 737,292 705,345 678,349 C 652,352 620,317 591,301 Z "/>
<Path x:Name="Alien1_Tooth1" Width="19" Height="27" Canvas.Left="411" Canvas.Top="606" Stretch="Fill" Fill="#FFFFFFFF" Data="F1 M 18,35 L 27,08 L 08,08 Z "/>
<Path x:Name="Alien1_Tooth2" Width="19" Height="27" Canvas.Left="432" Canvas.Top="609" Stretch="Fill" Fill="#FFFFFFFF" Data="F1 M 39,38 L 49,11 L 30,11 Z "/>
<Path x:Name="Alien1_Tooth3" Width="19" Height="27" Canvas.Left="453" Canvas.Top="612" Stretch="Fill" Fill="#FFFFFFFF" Data="F1 M 60,41 L 70,14 L 51,14 Z "/>
<Path x:Name="Alien1_Tooth4" Width="19" Height="27" Canvas.Left="477" Canvas.Top="612" Stretch="Fill" Fill="#FFFFFFFF" Data="F1 M 85,41 L 94,14 L 75,14 Z "/>
<Path x:Name="Alien1_Tooth5" Width="19" Height="27" Canvas.Left="500" Canvas.Top="608" Stretch="Fill" Fill="#FFFFFFFF" Data="F1 M 57,337 L 67,310 L 48,310 Z "/>
<Path x:Name="Alien1_LeftNail1" Width="19" Height="30" Canvas.Left="339" Canvas.Top="839" Stretch="Fill" Fill="#FF000000" Data="F1 M 77,94 L 79,64 L 84,66"/>
<Path x:Name="Alien1_LeftNail2" Width="30" Height="28" Canvas.Left="297" Canvas.Top="820" Stretch="Fill" Fill="#FF000000" Data="F1 M 36,78 L 67,56 L 56,47"/>
<Path x:Name="Alien1_LeftNail3" Width="27" Height="14" Canvas.Left="276" Canvas.Top="789" Stretch="Fill" Fill="#FF000000" Data="F1 M 16,22 L 43,29 L 43,15"/>
<Path x:Name="Alien1_RightNail1" Width="13" Height="25" Canvas.Left="580" Canvas.Top="838" Stretch="Fill" Fill="#FF000000" Data="F1 M 16,87 L 19,62 L 07,63"/>
<Path x:Name="Alien1_RightNail2" Width="25" Height="20" Canvas.Left="611" Canvas.Top="819" Stretch="Fill" Fill="#FF000000" Data="F1 M 65,75 L 49,45 L 38,58"/>
<Path x:Name="Alien1_RightNail3" Width="28" Height="14" Canvas.Left="633" Canvas.Top="787" Stretch="Fill" Fill="#FF000000" Data="F1 M 90,24 L 61,14 L 61,28"/>

The code sequence defines the z-order of the shapes. This is why the legs are defined before the body.

Points of Interest

To realize a radial gradient implementation for X11 was an interesting challenge. I was surprised, that displaying the result was such a straight forward process, even with the 'old' X11 API (except the pitfall concerning the size of the gradient bitmap). I think the result is convincing, only the edges of the figures look unattractive because of the missing X11 alpha blending capabilities.

History

  • The first version of this article is from 20. September 2016.
  • The second version of this article is from 26. September 2016.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Team Leader Celonis SA
Germany Germany
I am currently the CEO of Symbioworld GmbH and as such responsible for personnel management, information security, data protection and certifications. Furthermore, as a senior programmer, I am responsible for the automatic layout engine, the simulation (Activity Based Costing), the automatic creation of Word/RTF reports and the data transformation in complex migration projects.

The main focus of my work as a programmer is the development of Microsoft Azure Services using C# and Visual Studio.

Privately, I am interested in C++ and Linux in addition to C#. I like the approach of open source software and like to support OSS with own contributions.

Comments and Discussions

 
QuestionSome suggestions Pin
R.D.H.27-Sep-16 12:54
R.D.H.27-Sep-16 12:54 
AnswerRe: Some suggestions Pin
Steffen Ploetz28-Sep-16 8:37
mvaSteffen Ploetz28-Sep-16 8:37 
Many thanks for your feedback.

Can you pls. describe the idea of digitizing the color calculation a little more detailed?

Avoiding the inner square root leads to this results:
test 1: 459262 ticks before, 442007 ticks after square root elimination ==> 3.7% saving
test 2: 456730 ticks before, 459900 ticks after square root elimination ==> 0,7% spending
test 3: 472307 ticks before, 469264 ticks after square root elimination ==> 0,6% savings
test 4: 443541 ticks before, 457526 ticks after square root elimination ==> 3,1% spending
test 5: 450824 ticks before, 468275 ticks after square root elimination ==> 3,9% spending

Noise is a good option for the future. It will not affect the performance but the quality.

My first (very slow) approach prevented gaps by setting the next two neighbour pixel in advance. To implement such an gap prevention for the second approach is significant more difficult (but not impossible). Since gaps occure only the more the pixel to set is away from the center, it should be possible to find an opportunity for accelleration. My idea is to prefer to set a pixel twice than to determine whether a pixel has been set. That should be faster.
GeneralRe: Some suggestions Pin
R.D.H.29-Sep-16 2:08
R.D.H.29-Sep-16 2:08 
GeneralRe: Some suggestions Pin
Steffen Ploetz5-Oct-16 5:17
mvaSteffen Ploetz5-Oct-16 5:17 
GeneralRe: Some suggestions Pin
R.D.H.5-Oct-16 6:29
R.D.H.5-Oct-16 6:29 
GeneralRe: Some suggestions Pin
Steffen Ploetz14-Oct-16 3:18
mvaSteffen Ploetz14-Oct-16 3:18 
GeneralRe: Some suggestions Pin
Steffen Ploetz21-Oct-16 19:09
mvaSteffen Ploetz21-Oct-16 19:09 
QuestionCompile solution Pin
avisal20-Sep-16 6:05
professionalavisal20-Sep-16 6:05 
AnswerRe: Compile solution Pin
Steffen Ploetz20-Sep-16 7:54
mvaSteffen Ploetz20-Sep-16 7:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.