Download XamlAlienSokoban_RadialGradient_X11_32.zip Mono project including full source and executable
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:
- 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].
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"/>
- 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.
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.
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
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.
for (int y = 0; y < bitmap.Height; y++)
...
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.
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.
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);
...
if (loopScanlineRtL)
{
double buffer = firstCenteredEllipseAngle;
firstCenteredEllipseAngle = lastCenteredEllipseAngle;
lastCenteredEllipseAngle = buffer;
}
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.
if (!loopScanlineRtL)
{
if (lastX + 1 < x)
{
for (int subX = lastX + 1; subX < x; subX++)
{
bitmap.SetPixel (subX, y, c);
countPixelSettings++;
}
}
}
else
{
if (lastX - 1 > x)
{
for (int subX = lastX - 1; subX > x; subX--)
{
bitmap.SetPixel (subX, y, c);
countPixelSettings++;
}
}
}
lastX = x;
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.
• 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));
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);
...
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.
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:
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);
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.
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:
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 . 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 ) 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 .
The next image shows the sample application on OPEN SUSE 11.3 Linux 32 bit EN and GNOME desktop.
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.