矢量图形渲染的动画时钟






4.91/5 (78投票s)
2004年4月19日
8分钟阅读

362157

10
演示如何使用 MyXaml 和矢量图形引擎创建一个模拟时钟
目录
引言
在我上一篇文章中,我演示了如何使用 MyXaml 创建一个简单的博客阅读器。在本文中,我想演示如何结合声明式标记来使用VG.net 的运行时引擎创建矢量图形应用程序。特别是,我将演示创建这个工作时钟的代码。
初始标记
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<MyXaml
xmlns:def="Definition"
xmlns="Prodige.Drawing, Prodige.Drawing"
xmlns:pds="Prodige.Drawing.Styles, Prodige.Drawing">
<Picture Name="Clock">
</Picture>
</MyXaml>
初始标记声明了程序集命名空间,并将它们与 xmlns
前缀关联起来。在此情况下,默认命名空间是 Prodige.Drawing
矢量图形运行时引擎。Picture
类是矢量图形元素的容器。
创建容器窗体
通常,一个或多个 Picture
对象绘制在 Canvas
上,Canvas
是一个用户控件,可以添加到 Form
中。 由于 VG.net 设计器可以直接生成 MyXaml
标记,我构建了一个小型加载程序 vgLoader.exe。 该加载程序指示解析器实例化 Picture
,然后调用 Picture
的 DisplayInForm
方法。 这将返回一个初始大小的 Form
,然后可以显示该窗体。实际加载的代码如下:
Parser parser=new Parser();
object picture=parser.LoadForm(filename, "*", null, null);
Type type=picture.GetType();
MethodInfo mi=type.GetMethod("DisplayInForm");
Form form=mi.Invoke(picture, new object[] {new Size(10, 10)}) as Form;
form.ShowDialog();
由于加载程序不知道 Picture
的名称,它使用“*
”通配符来指示解析器实例化 <MyXaml>
标签后的第一个遇到的类。
时钟的特征
以下各节将讨论时钟的各个部分是如何构建的。
框架
<?xml version="1.0" encoding="utf-8" standalone="no"?>
MyXaml
xmlns:def="Definition"
xmlns="Prodige.Drawing, Prodige.Drawing"
xmlns:pds="Prodige.Drawing.Styles, Prodige.Drawing">
<Picture Name="Clock">
<Elements>
<Ellipse Name="outerRim" Location="100, 100" Size="200, 200"
StyleReference="Rim" DrawAction="Fill" />
<Ellipse Name="innerRim" Location="110, 110" Size="180, 180"
StyleReference="Rim" DrawAction="Fill">
<Fill>
<pds:LinearGradientFill Angle="225" />
</Fill>
</Ellipse>
</Elements>
<Styles>
<pds:Style Name="Rim">
<pds:Fill>
<pds:LinearGradientFill GradientType="TwoColor" Angle="45"
StartColor="192, 192, 255" />
</pds:Fill>
</pds:Style>
</Styles>
</Picture>
</MyXaml>
时钟框架由两个圆(Ellipse
类)组成,一个绘制在另一个内部。 使用线性渐变填充来创建从左上角投射到时钟上的光线阴影效果。在此标记中,内外边缘使用相同的样式,但内边缘会覆盖 Angle
属性。
表盘
<Ellipse Name="face" Location="114, 114" Size="173, 173"
StyleReference="Face" DrawAction="Fill" />
<pds:Style Name="Face">
<pds:Fill>
<pds:LinearGradientFill GradientType="TwoColor" Angle="45"
EndColor="0, 0, 0" StartColor="125, 123, 168" />
</pds:Fill>
</pds:Style>
创建了第三个椭圆和一个附加样式来添加表盘。
阴影
<Ellipse Name="shadow" Location="102, 102" Size="200, 200"
StyleReference="ClockShadow" DrawAction="Fill" />
<pds:Style Name="ClockShadow">
<pds:Fill>
<pds:SolidFill Color="63, 0, 0, 0" Opacity="0.247058824" />
</pds:Fill>
</pds:Style>
阴影是另一个椭圆和样式。
对象是逐个绘制的,因此,到目前为止矢量图形元素的实际顺序是:
<Elements>
<Ellipse Name="shadow" Location="102, 102" Size="200, 200"
StyleReference="ClockShadow" DrawAction="Fill" />
<Ellipse Name="outerRim" Location="100, 100" Size="200, 200"
StyleReference="Rim" DrawAction="Fill" />
<Ellipse Name="innerRim" Location="110, 110" Size="180, 180"
StyleReference="Rim" DrawAction="Fill">
<Fill>
<pds:LinearGradientFill Angle="225" />
</Fill>
</Ellipse>
<Ellipse Name="face" Location="114, 114" Size="173, 173"
StyleReference="Face" DrawAction="Fill" />
</Elements>
刻度
<Picture>
<TextAppearance>
<pds:TextAppearance Color="192, 192, 255" Size="18"
FaceName="Maiandra GD" RenderingHint="AntiAliasGridFit"
SizeUnit="World" />
</TextAppearance>
<Elements>
...
Picture
对象的 TextAppearance
属性被实例化为默认文本外观,用于每个数字。
<Rectangle Name="one" Text="1" Location="220, 124.3782" Size="30, 30"
StyleReference="Numeral" DrawAction="Fill" />
<Rectangle Name="two" Text="2" Location="245.6218, 150" Size="30, 30"
StyleReference="Numeral" DrawAction="Fill" />
<Rectangle Name="three" Text="3" Location="255, 185" Size="30, 30"
StyleReference="Numeral" DrawAction="Fill" />
<Rectangle Name="four" Text="4" Location="245.6218, 220" Size="30, 30"
StyleReference="Numeral" DrawAction="Fill" />
<Rectangle Name="five" Text="5" Location="220, 245.6218" Size="30, 30"
StyleReference="Numeral" DrawAction="Fill" />
<Rectangle Name="six" Text="6" Location="185, 255" Size="30, 30"
StyleReference="Numeral" DrawAction="Fill" />
<Rectangle Name="seven" Text="7" Location="150, 245" Size="30, 30"
StyleReference="Numeral" DrawAction="Fill" />
<Rectangle Name="eight" Text="8" Location="124.3782, 220" Size="30, 30"
StyleReference="Numeral" DrawAction="Fill" />
<Rectangle Name="nine" Text="9" Location="115, 185" Size="30, 30"
StyleReference="Numeral" DrawAction="Fill" />
<Rectangle Name="ten" Text="10" Location="124.3782, 150" Size="30, 30"
StyleReference="Numeral" DrawAction="Fill" />
<Rectangle Name="eleven" Text="11" Location="150, 124.3782" Size="30, 30"
StyleReference="Numeral" DrawAction="Fill" />
<Rectangle Name="twelve" Text="12" Location="185, 115" Size="30, 30"
StyleReference="Numeral" DrawAction="Fill"/>
<pds:Style Name="Numeral">
<pds:Fill>
<pds:SolidFill Opacity="0" />
</pds:Fill>
</pds:Style>
每个数字都放置在表盘的顶部,因此在 Elements
列表中出现在“face” Ellipse
之后。如果您想知道我是否手动编码了精度到万分位的放置,答案是“否”。由于我对矢量图形的理解还比较新,Frank Hileman 在 VG.net 设计器中绘制了时钟。我问 Frank 是如何做到的,他回答如下:
首先,我创建了“十二”,并将其定位在顶部。我选择了“十二”,并将 TransformationReference
Type
属性设置为“Absolute”。然后,我将 TransformationReference Location
改为圆的中心。
<TransformationReference>
<TransformationReference Location="200, 200" Type="Absolute"/>
</TransformationReference>
现在,任何对 Rotation 的更改都将围绕该 Location 进行。我复制粘贴了“十二”,在同一位置创建了一个相同的对象。我们将其命名为“三”。将 Rotation 属性更改为 90,并将 Text 属性更改为“3”。现在您已经拥有了围绕时钟中心旋转的文本。
现在我们需要移除 Rotation,但要相对于文本中心,而不是时钟中心。选择“三”。右键单击 TransformationReference 属性,然后单击“Reset”。参考点将返回到 Center,但对象不会移动。现在右键单击 Rotation,然后单击“Reset”。Rotation 将消失,但文本不会移动。
我将“十二”对象复制了 11 次,每次都通过 30 度的倍数设置 Rotation 属性,更改 Name 和 Text 属性,并按顺序对 TransformationReference 和 Rotation 进行 Reset。
分针
<Group Name="minute" StyleReference="Minute">
<TransformationReference>
<TransformationReference Location="200, 200" Type="Absolute" />
</TransformationReference>
<Elements>
<Path Name="leftMinute" DrawAction="Fill">
<PathPoints>
<PathPoint Point="200.101, 120" Type="Start" />
<PathPoint Point="199.7635, 131.6271" Type="Control1" />
<PathPoint Point="195, 194.9518" Type="Control2" />
<PathPoint Point="195.0503, 198.8948" Type="EndBezier" />
<PathPoint Point="195.1006, 202.8378" Type="Control1" />
<PathPoint Point="197.7291, 205.1745" Type="Control2" />
<PathPoint Point="200, 204.9767" Type="EndBezier" />
<PathPoint Point="200.2, 204.5694" Type="EndLine" />
</PathPoints>
</Path>
<Path Name="rightMinute" DrawAction="Fill" Scaling="1, -1.213767"
Rotation="180">
<PathPoints>
<PathPoint Point="205.09, 197.4994" Type="Start" />
<PathPoint Point="202.8521, 197.6623" Type="Control1" />
<PathPoint Point="200.1495, 195.748" Type="Control2" />
<PathPoint Point="200.0996, 192.4994" Type="EndBezier" />
<PathPoint Point="200.0498, 189.2508" Type="Control1" />
<PathPoint Point="204.7664, 137.0788" Type="Control2" />
<PathPoint Point="205.09, 127.4994" Type="EndBezier" />
</PathPoints>
</Path>
</Elements>
</Group>
<pds:Style Name="Minute">
<pds:Fill>
<pds:LinearGradientFill Bounds="0, 0.8, 1.3, 1.3"
GradientType="TwoColorBell" Angle="140" EndColor="202, 222, 255"
StartColor="0, 0, 128" />
</pds:Fill>
</pds:Style>
我问 Frank 是如何制作指针的,他的回答是:
我必须承认,里面有一个技巧。方法如下:我为指针的一半创建了一个 3 点样条。然后我将其转换为 Path,并稍微调整了控制点。然后我复制粘贴,在同一位置创建了一个相同的对象。为了镜像,我将 scale X 属性设置为 -1(用于分针)。然后我将指针移到右边,使用网格吸附将其与其他指针对齐。由于每个 Path 都显示一个线性渐变,但其中一个显示方向相反(由于负比例),它们一起给它提供了 3D 效果。
那么为什么在生成的 XML 中会看到这种奇怪的缩放? 这是因为在创建两个半边指针后我又调整了它们的大小。由于左半边不需要 -1 缩放,设计器转换了 Path 中的点。我可以移除右半边奇怪的缩放,使用 ApplyTransformation,但我需要保留右半边中的负数缩放,以反转渐变(这样我就可以为两者保持相同的 Style)。默认情况下,当您调整大小时,如果对象已缩放,设计器不会将变换应用于点。因此,右半边保留了缩放,但经过了修改。如果我在复制粘贴之前就正确调整了左半边的尺寸,您就不会看到这种奇怪的缩放。
时针的做法类似,但我将其水平制作。
LinearGradientFill 的 Bounds 和 Angle 被仔细选择,以使渐变的暗边与 Path 的角度对齐。
现在是技巧。指针的中间有一个小线(在较小的比例下仍然存在,我没有完全消除它)。这是由于 GDI+ 中的填充算法未能完美对齐填充区域的边缘,因此您会看到背景。我为左半边添加了一个额外的点来覆盖它。我也尝试通过调整端点来覆盖它,但这从未真正奏效。我现在意识到更好的选择是绘制一条单像素的线在中间,位于两个填充的半边后面。
我们希望分针成为最下面的指针,因此它在刻度之后立即声明,并具有自己的样式。在此标记中,正在声明一个组(元素的复合体),每个半边分针一个。Path
类定义了一组图形,每个图形包含一组直线段和曲线段(路径点由设计器确定,而不是由我!)。
还要注意,整个组使用 TransformationReference
来指定组的绝对参考点。这允许我们围绕时钟中心旋转分针路径的起点。
时针
<Group Name="hour" StyleReference="Hour">
<TransformationReference>
<TransformationReference Location="200, 200" Type="Absolute" />
</TransformationReference>
<Elements>
<Path Name="leftHour" DrawAction="Fill" Rotation="270">
<PathPoints>
<PathPoint Point="225.1051, 179.8949" Type="Start" />
<PathPoint Point="217.4753, 179.0615" Type="Control1" />
<PathPoint Point="188.4821, 174.8949" Type="Control2" />
<PathPoint Point="179.3263, 174.8949" Type="EndBezier" />
<PathPoint Point="170.1706, 174.8949" Type="Control1" />
<PathPoint Point="170.1053, 178.2542" Type="Control2" />
<PathPoint Point="170.1706, 179.8949" Type="EndBezier" />
<PathPoint Point="170.4581, 180.1053" Type="EndLine" />
</PathPoints>
</Path>
<Path Name="rightHour" DrawAction="Fill" Scaling="1.831152, -1"
Rotation="270">
<PathPoints>
<PathPoint Point="217.3672, 179.8948" Type="Start" />
<PathPoint Point="213.2005, 179.0614" Type="Control1" />
<PathPoint Point="197.3672, 174.8948" Type="Control2" />
<PathPoint Point="192.3672, 174.8948" Type="EndBezier" />
<PathPoint Point="187.3672, 174.8948" Type="Control1" />
<PathPoint Point="187.3315, 178.2541" Type="Control2" />
<PathPoint Point="187.3672, 179.8948" Type="EndBezier" />
<PathPoint Point="187.5243, 180.1052" Type="EndLine" />
</PathPoints>
</Path>
</Elements>
</Group>
<pds:Style Name="Hour">
<pds:Fill>
<pds:LinearGradientFill Bounds="0, 0.8, 1.3, 1.3"
GradientType="TwoColorBell" Angle="140" EndColor="202, 222, 255"
StartColor="0, 0, 128" />
</pds:Fill>
</pds:Style>
时针几乎与分针相同——它由一个元素组组成,包含两个路径,一个用于时针的左侧,一个用于右侧。使用单独的样式。
秒针
<Polyline Name="second" StyleReference="Second" DrawAction="Edge">
<TransformationReference>
<TransformationReference Location="200, 200" Type="Absolute" />
</TransformationReference>
<Points>
<Vector X="200" Y="200" />
<Vector X="200" Y="135" />
</Points>
</Polyline>
<pds:Style Name="Second">
<pds:Stroke>
<pds:Stroke Color="255, 255, 255" EndCap="Round" StartCap="Round"
Width="1" />
</pds:Stroke>
</pds:Style>
作为一条直线,秒针更简单,它被实现为一个带有起点和终点的 PolyLine
。
时钟动画
现在唯一剩下的就是让时钟报时了! 我们需要做三件事:
- 向
System.Windows.Forms
命名空间添加一个xmlns
,以便完整的命名空间列表现在如下所示:<MyXaml xmlns:def="Definition" xmlns="Prodige.Drawing, Prodige.Drawing" xmlns:pds="Prodige.Drawing.Styles, Prodige.Drawing" xmlns:wf="System.Windows.Forms">
- 实例化一个
Timer
(这就是System.Windows.Forms
命名空间的原因)。<Picture Name="Clock"> <wf:Timer Tick='OnTick' Interval='10' Enabled='true'/> ...
- 实现事件处理程序。
<def:Code language='C#'> <reference assembly="System.Windows.Forms.dll"/> <reference assembly="System.Xml.dll"/> <reference assembly="myxaml.dll"/> <reference assembly="Prodige.Drawing.dll"/> <![CDATA[ using System; using System.ComponentModel; using System.Diagnostics; using System.Windows.Forms; using MyXaml; using Prodige.Drawing; class AppHelpers { public Parser parser; public AppHelpers() { parser=Parser.CurrentInstance; } public void OnTick(object sender, EventArgs e) { DateTime n = DateTime.Now; Polyline second=(Polyline)picture.Elements["second"]; Group minute=(Group)picture.Elements["minute"]; Group hour=(Group)picture.Elements["hour"]; second.Rotation = 360F * ((n.Second+ n.Millisecond/1000F)/60F); minute.Rotation = 360F * (n.Minute + n.Second/60F)/60F; hour.Rotation = 360F * (n.Hour + n.Minute/60F)/12F; } } ]]> </def:Code>
在编译后的程序集中设置处理程序
如果您不喜欢将内联代码与标记混在一起,您可以在自己的程序集中设置事件处理程序。在本文的开头,我展示了加载程序的代码片段:
Parser parser=new Parser();
object picture=parser.LoadForm(filename, "*", null, null);
Type type=picture.GetType();
MethodInfo mi=type.GetMethod("DisplayInForm");
Form form=mi.Invoke(picture, new object[] {new Size(10, 10)}) as Form;
form.ShowDialog();
通过为事件指定目标对象(将第一个 null
更改为“this
”或任何包含事件处理程序的类的实例)。
object picture=parser.LoadForm(filename, "*", this, null);
您可以将 OnTick
方法直接复制到您的程序集中,解析器将自动将事件连接到您的程序集。
结论
MyXaml
与第三方运行时协同工作的能力提供了一种极其简便的方式来为应用程序插入功能。与 C# 代码相比,标记的大小不到 1/10,而且我认为它更具可读性,更容易编辑。
并且,与 VG.net 免费的运行时矢量图形引擎结合,可以编写出一些真正令人惊叹的应用程序。我希望本文能够激发大量的讨论,并激发您对矢量图形之美的兴趣!
下载文件中提供了矢量图形的附加示例。
注释
- 此演示中包含的 MyXaml 版本是下一个 Beta 版(0.95)的预发行版。您可以从 http://myxaml.tigris.org/ 上的 CVS 站点下载源代码,或者等待我发布下一个版本。
- 此演示中的矢量图形引擎不一定是最新版本。您可以在 http://www.vgdotnet.com/ 下载最新的运行时矢量图形引擎。(显然,VG 引擎没有源代码。但运行时是免费且完全文档化的)。VG.net 还提供了一个为期 30 天、有时间限制的 Beta 版设计器。
延伸阅读
许可证
本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。