简单的径向渐变实现,带有 XAML 示例应用程序(用于 X11,但无需任何外部库)。





5.00/5 (3投票s)
如何计算径向渐变并在 System.Drawing.Bitmap 上绘制输出。以及如何使用此位图作为平铺像素图来填充任何路径。
下载 XamlAlienSokoban_RadialGradient_X11_32.zip Mono 项目,包含完整的源代码和可执行文件
下载 XamlAlienSokoban_RadialGradient2_X11_32.zip (第二种方法) Mono 项目,包含完整的源代码和可执行文件
引言
我正在寻找一种径向渐变算法实现,该实现可以在不集成任何外部库(尤其是 Cairo)的情况下使用,用于我的 Roma Widget Set (C# X11) 项目。(我将介绍的结果是通用的,也可以用于 Win32 和 Windows Forms。)
这更像是一个解决问题的挑战,而不是优于任何专业径向渐变实现的途径——因为我从程序员的角度而不是数学家的角度来审视这个问题。
径向渐变有两种解释
- 径向渐变,产生看起来像/可解释为 3D 圆锥体的 2D 俯视图,并且需要一个椭圆/圆锥底座(centerX, centerY, radiusX, radiusY)以及一个焦点/顶点(originX, originY)。基本原理请参见 OpenVG 规范。例如,由 WPF 实现。
补充:我对初步方法的性能不满意。如果您只想阅读最终解决方案,可以跳过“背景(初步方法)”一章。
图像显示了一个 radiusX != radiusY 且 [centerX, centerY] != [originX, originY] 的示例。
用于图像的颜色渐变停止定义是<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"/>
- 径向渐变,产生看起来像/可解释为 3D 管的 2D 投影,并且需要一个起始圆/管起始点(startX, startY, startRadius)以及一个结束圆/管结束点(endX, endY, endRadius)。例如,由 HTML5 canvas 和 Cairo 实现。如果结束圆完全包含起始圆且起始圆半径接近 0.0,那么第二种径向渐变解释会产生与第一种(左图)类似的结果。否则,它会产生一个清晰可辨的管投影(右图)。
补充:我找到一个非常古老且稳定的(因为它被 X11 扩展 Xrender 和 Cairo 使用)并且文档非常完善的 C 源文件,该文件实现了这种方法,位于 pixman 的 pixman-radial-gradient.c 文件中。它也被无情地复制到了 Chrome/Android/Mozilla 的着色器库 'skia' 的 SkGradientShader.cpp 文件中。
左图显示了一个起始圆完全包含在结束圆内的示例。
右图显示了一个起始圆完全位于结束圆外部(右上方)的示例。
用于图像的颜色渐变停止定义是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');
我将要介绍的径向渐变算法实现是基于第一种方法的。
背景(初步方法)
我在网上找到了两篇文章,它们讨论了理论基础
- Dorothy Browser 博客文章 如何实现 HTML5 Canvas 径向渐变。
- 以及俄勒冈州立大学的文章 梯度和方向导数。
如果您知道其他参考资料,欢迎提供反馈!
一些基本概念
像素的颜色是基于颜色渐变停止定义计算的,这些定义在偏移量范围从 0.0 到 1.0 内定义颜色值。渐变停止偏移量可以解释为从焦点/顶点(originX, originY)到像素的距离dP,归一化为从焦点/顶点(originX, originY)到椭圆边界的距离dE。
假定距离dP为 √ (ΔxP² + ΔyP²) = √ (15² + 20²) = 25,距离dE为 √ (ΔxE² + ΔyE²) = √ (43.5² + 58²) = 72.5 - 归一化距离dN为 dP/ dE = 25/72.5 = 0.3448.
假定相关的颜色渐变停止定义是
... <GradientStop Color="#FFFF0000" Offset="0.34322"/> <!-- Stop5 --> <GradientStop Color="#FFFFFFFF" Offset="0.36017"/> <!-- Stop6 --> ...
像素颜色cP[aarrggbb] 计算为 (1.0 -dN) * Stop5[aarrggbb] + dN * Stop6[aarrggbb]。
因此cP[aarrggbb] = (1.0 -0.3448) * Stop5[FFFF0000] +0.3448 * Stop6[FFFFFFFF] = [58580000] + [A7A7A7A7] = [FFFFA7A7]。
计算策略
首先 实现的径向渐变算法逐行扫描位图(0 <= y < bitmap.Height)。
其次 对于 centerY 坐标上方的每一条扫描线,一个以椭圆中心为原点的角度从 1.5 * PI
扫到 -0.5 * PI
。对于 centerY 坐标下方的每一条扫描线,一个以椭圆中心为原点的角度从 0.5 * PI
扫到 2.5 * PI
。
这样做是为了
- 确保扫描线的所有像素都已计算,并且
- 椭圆边界点pE可以轻松计算(boundaryX² / radiusX² + boundaryY² / radiusY² = 1)。
第三 椭圆点pE坐标被转换为pE(F)坐标,相对于焦点/顶点 pF。由于给定了扫描线的 y 坐标,像素的扫描线 x 坐标pS可以基于 pF使用此方程轻松计算pS.X = pE(F).X / |pE(F).Y| * |pS.Y| 位图边界外的扫描线 x 坐标将被丢弃。
第四 像素颜色按如上所述计算并设置为像素。
计算问题
• 第一个问题是速度。目前,通过位图扫描线的自上而下的循环是单线程实现的。相反,由于扫描线之间没有滞后效应,每一条扫描线都可以单独计算(多线程)。唯一挑战是竞争的位图访问,这可以通过托管像素数组缓冲区和从托管像素数组缓冲区到位图位的最终传输来解决。
// Loop through the scan lines. for (int y = 0; y < bitmap.Height; y++) ... // Could be optimized to: Parallel.For(...) ...
• 第二个问题是准确性。该算法对于接近 PI
和 0.0
(或 2 * PI
)值的中心原点角度不准确。角度值 PI
和 0.0
根本无法计算(第三步中除以零)。相反,接近 PI
和 0.0
值的角度可以基于扫描列而不是扫描线来计算,但这需要实现第二种计算策略。目前,准确性经过调整以足够,但这会导致对同一像素进行大量重复计算。角度为 PI
和 0.0
的扫描线通过其正上方的扫描线进行插值。
// 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); } } }
• 第三个问题是鲁棒性。该算法在焦点/顶点位于椭圆边界外部,尤其是中心上方和右侧以及中心下方和左侧的情况下并不鲁棒。这是因为在这两种情况下,扫描线像素都有两个中心原点角度,它们计算一个像素的颜色。
在这些情况下,必须交换椭圆中心原点的角度范围的起始和结束角度,以计算有用的结果。
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)) { ... }
• 第四个问题是间隙。有时,尤其是在接近 PI
和 0.0
(或 2 * PI
)的角度值时,三角函数的精度不足以计算至少一个像素的颜色值。为了防止间隙(通常出现在扫描线的开头或结尾),将计算出的第一个颜色传播到所有之前的像素,并将计算出的最后一个颜色传播到位图边界。
// 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++; } } }
通过所有这些额外的努力,实现的径向渐变算法也能为特殊情况计算出有用的结果。下图显示了四种特殊情况(焦点/顶点位于椭圆边界外部)。结果与 WPF 使用相同起始值产生的效果相同。
• 最后一个问题是关于渐变位图大小的陷阱。渐变位图必须足够大,以在任何情况下都包含用径向渐变填充的图形的完整轮廓。否则,可能会发生意外的访问冲突 (SIGSEGV) 错误,部分错误在问题触发后才发生(我猜想它们发生在该平铺像素图在 XFillRectangle()
、XFillPolygon()
调用中被用作图形填充时)。
目前,渐变位图会放大 8px 并对齐到 8px 的倍数,以避免这些访问冲突错误。
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));
但是,这种方法需要调整渐变位图的一半(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);
背景(第二种方法)
性能问题
我对初步方法的性能不满意(原始代码)。我重新组织了数据结构,并准备了多线程来让每个扫描线由一个单独的线程计算(重构代码)。在双核虚拟机上,一个 208 x 120px 位图的测量值如下:
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
我切换到多线程,并寄予厚望它会快得多——但是
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
显然,多线程产生的开销大于节省的开销,即使是 50 个线程也不如单线程重构代码。这非常令人失望。
因此,我想检查一下我的初步方法与绘制径向渐变最简单的方法:绘制椭圆相比,效果如何。
计算策略
绘制径向渐变最简单的方法是绘制具有均匀颜色的同心椭圆轮廓。这种算法只需要确保每个位图像素至少被设置一次。最有效的算法将计算椭圆,使得每个位图像素恰好被设置一次——但这个目标非常难以实现。
下图显示了这种方法的原理。
我在最大半径 r(其中 r = √ (rX² + rY²) 对于椭圆公式 pX²/rX² + pY²/rY² = 1)处选择了 1.5(33%)的像素重叠。由于我的实现应该支持焦点/顶点位置在椭圆边界外,它也必须考虑外部距离。
// 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);
如果径向渐变的焦点/顶点位置不等于径向渐变的中心位置,则要绘制的每个椭圆轮廓在从径向渐变中心位置到径向渐变焦点/顶点位置的直线上都有一个不同的椭圆中心。
// 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;
}
较大的椭圆比较小的椭圆与更多的位图点相交。我试图考虑到这一点。
// 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)
{
...
}
这种算法的性能比初步方法快了近 10 倍。
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
基于最近的经验,我放弃了切换到多线程。
使用代码
所有径向渐变算法代码(包括初步方法和第二种方法)都包含在我的 Roma Widget Set (C# X11) 项目的 class X11RadialGradientBrushInfo
方法 TilePixmap(...)
中,从版本 开始。为了测试实现并展示算法的适用性,本文提供了一个示例应用程序。
示例应用程序是一个基于 MVVM(模型-视图-视图模型)设计模式的 X11 应用程序,通过 XAML 使用 Roma Widget Set (Xrw) 定义 UI。它是用于 X11 的零依赖 GUI 应用程序框架(它只需要免费 Mono 标准安装的程序集和免费 X11 发行的库;它不特别需要 GNOME、KDE 或商业库),并且完全用 C# 实现。文章 为 X11 编写 XAML 功能区应用程序 描述了 Xrw 的 XAML 包装器的基础知识。
由于径向渐变算法实现基于 System.Drawing.Bitmap
类,因此将其移植到支持 System.Drawing.Bitmap 类的 X11 以外的平台很容易。
示例应用程序基于 WPF Alien Sokoban by Daniel Vaughan 的想法和背景图像。 示例应用程序使用 Mono Develop 2.4.1,Mono 2.8.1,在 OPEN SUSE 11.3 Linux 32 位 EN 和 GNOME 桌面环境下编写。移植到任何更旧或更新的版本应该都不是问题。示例应用程序的解决方案包含四个项目(完整的源代码均可下载)。
- XamlAlienSokoban 包含示例应用程序的源代码。
- XamlPreprocessor 包含 XAML 预处理器的源代码。
- Xrw 包含 Roma Widget Set (Xrw) 和(在
中引入)API 的 HTML 文档。
- X11Wrapper 为 Xlib/X11 调用 libX11.so 定义函数原型、结构和类型。
所有项目都包含完整的源代码。Xrw 和 X11Wrapper 项目代表了 Roma Widget Set 预期版本 的预览。
下图显示了在 OPEN SUSE 11.3 Linux 32 位 EN 和 GNOME 桌面环境下的示例应用程序。
该图像包含四个外星人,每个外星人都完全由 XAML 代码组成。身体(ellipse
)和腿(path
)显示从浅绿色到绿色的径向渐变。眼睛(ellipse
)显示从黑色、白色、红色、白色到灰色的径向渐变。
这是“正面”外星人(Alien1
)的 XAML 代码。
<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"/>
代码序列定义了形状的 Z 顺序。这就是为什么腿定义在身体之前。
关注点
为 X11 实现径向渐变算法是一个有趣的挑战。我惊讶地发现,即使使用“旧”的 X11 API(除了关于渐变位图大小的陷阱),显示结果的过程也如此直接。我认为结果令人信服,只是图形的边缘看起来不吸引人,因为缺少 X11 alpha 混合功能。
历史
- 本文的第一个版本写于 2016 年 9 月 20 日。
- 本文的第二个版本写于 2016 年 9 月 26 日。