堆叠几何画笔工厂






4.95/5 (63投票s)
一款具有插件架构和自定义XAML的几何视觉画笔生成器。
引言
本文介绍了如何使用形状创建效果,此外,还介绍了如何为WPF构建插件架构以及通过属性控制XAML的序列化。它最初是为了创建一个发光的霓虹星作为图标而开始的,最终却成为一个多功能的视觉画笔工厂。很快就显而易见,该解决方案可以得到扩展。我决定创建一个具有以下特征的图形效果创建应用程序:
- 能够以混合搭配的方式使用多种不同的几何图形源(Geometry Sources)和效果(Effect Factories)。
- 几何源和效果工厂应该能够在以后添加,而无需我修改现有代码。
- 这不仅仅是为了生成漂亮的图像。它需要生成相当简洁的XAML,我可以将其粘贴到另一个项目中并获得相同的图像。
大多数时候,我们在Illustrator或Photoshop中通过复制图像并将其堆叠在彼此之上来创建效果。然后,每个图层都会以某种方式进行转换。通过这种方式可以相当容易地创建一些引人注目的效果。以下是生成堆叠形状的基本计划:
- 从Geometry Source类创建几何图形。
- 从Effect Factory创建效果集合。
- 使用几何图形创建Path集合。
- 将效果应用于Path。
- 按顺序将Path作为Canvas的Children添加。
- 从此Canvas创建Visual Brush,我们就完成了。
背景
访问TutVid.com等互联网图形教程网站并观看一些视频可能会很有用,也可能很有趣。特别是,Comic Effect Factory受到他们一个视频的启发。如果您在YouTube上搜索“Illustrator tutorial”,您还会找到很多很棒的信息。
值得一提的是,本文最初是为了改进PolyStars的外观而产生的。如果您想更详细地了解polystar几何图形,可以在那里找到。
创建几何图形
效果将通过制作形状的相同副本然后修改它们来创建。以下方法将复制大多数形状,即使它被拒绝了,也值得一看。唯一的限制是不要直接暴露泛型类型,因为这会导致XamlWriter
失败。
private ObservableCollection<shape> CopyShapes(Shape shape, int copyCount)
{
ObservableCollection<shape> shapeStack = new ObservableCollection<shape>();
string pString = XamlWriter.Save(shape);
for (int i = 0; i < copyCount; i++)
{
StringReader sr = new StringReader(pString);
XmlTextReader xmlr = new XmlTextReader(sr);
Shape cShape = (Shape)XamlReader.Load(xmlr);
shapeStack.Add(cShape);
}
return shapeStack;
}
最初,此解决方案中使用了与此方法非常类似的方法。它有效,但不令人满意。在这种情况下,使用序列化似乎有点过头了。
在框架提供的继承自Shape类的类中,Path
类值得一提,因为它允许我们插入其几何图形。幸运的是,多个Path
实例共享相同的几何图形没有问题。问题在于我们如何创建几何图形,其中有三种主要情况:
- 我们可以控制该类,因此可以公开受保护的几何图形。
- 几何图形的源是形状。
- 几何图形的源是某些文本。
IGeometrySource
创建IGeometrySource
接口是为了提供ExternalGeometryShape
实例的Geometry
源。
[CustomXaml]
public interface IGeometrySource:IFactory
{
//Main Method analogus to CacheDefiningGeometry
Geometry CreateGeometry();
// Supplies geometry Analogus to GetDefiningGeometry
Geometry Geometry { get; set; }
}
CreateGeometry
方法足以确保实现IGeometrySource
接口的类可以用于提供Geometry
对象,而Geometry
属性由StackedGeometry
用于缓存。我有权选择修改类以支持此接口,就像我为PolyStar
所做的那样,或者创建一个实现该接口的包装类,就像为TextBlock
和Shape
所做的那样。IFactory
接口用于插件系统以及更新显示。
Shapes
所有继承自Shape
的类都实现了受保护的DefiningGeometry
方法。我们的第一反应可能是继承自Shape
然后公开该方法。不幸的是,继承自Shape
的所有框架类都是sealed
的。如果我们真的想,无论它们如何暴露,我们都可以始终通过反射来调用任何类。
private Geometry GetHiddenDefiningGeometry(Shape shape)
{
Type shptype = shape.GetType();
MethodInfo mi = shapeType.GetMethod("CacheDefiningGeometry",
BindingFlags.NonPublic | BindingFlags.Instance);
PropertyInfo pi = shapeType.GetProperty("DefiningGeometry",
BindingFlags.NonPublic | BindingFlags.Instance);
mi.Invoke(Shape, null);
return (Geometry)pi.GetValue(mShape, null);
}
让我们看一下Polygon
类的方法(感谢Reflector)。
几乎所有工作都在CacheDefiningGeometry
方法中完成。如果所讨论的形状嵌入在XAML中,那么它很有可能在我们尝试调用其几何图形之前已经被渲染了,但先调用CacheDefiningGeometry
方法更安全。
private void btValidate_Click(object sender, RoutedEventArgs e)
{
ValidationButton but = (ValidationButton)sender;
ShapeSource ss = (ShapeSource)but.DataObject;
TextBox tb = (TextBox)but.ObjectToValidate;
try
{
StringReader sr = new StringReader(tb.Text);
XmlReader xmlReader = XmlReader.Create(sr);
Shape shp = XamlReader.Load(xmlReader) as Shape;
if (shp != null)
{
ss.Shape = shp;
}
}
catch { }
}
这是用于获取用户输入并从中创建形状的代码。值得注意的是,如果我们在不调用CacheDefiningGeometry
方法的情况下调用DefiningGeometry
属性,它将始终为null
。这种错误很容易犯。上面的代码可以工作;但是,它的速度不是很快,因为GetProperty
和GetMethod
方法很慢。我们通常听说反射的用途是允许使用在编译时未知的程序集和成员,但在这种情况下,我们已经知道要调用的成员。它们只是碰巧不是public
,所以让我们用static
s来完成所有工作。考虑这段代码:
[GeometrySource]
public class ShapeSource:IGeometrySource
{
// ...
private static PropertyInfo sPolygonDefiningGeometry;
//...
static ShapeSource()
{
//...
Type shptype = typeof(Polygon);
sPolygonDefiningGeometry = shptype.GetProperty("DefiningGeometry",
BindingFlags.NonPublic | BindingFlags.Instance);
//...
}
public Geometry CreateGeometry()
{
Type shapeType = mShape.GetType();
if (shapeType ==typeof(Polygon))
{
return (Geometry)sPolygonDefiningGeometry.GetValue(mShape, null);
}
else if //...
else
{
Type shptype = mShape.GetType();
PropertyInfo pi = shptype.GetProperty("DefiningGeometry",
BindingFlags.NonPublic | BindingFlags.Instance);
return (Geometry)pi.GetValue(mShape, null);
}
}
//..
}
这段代码应该以与普通方法相当的速度运行,因为慢方法只需要调用一次。此时,我感到有义务说明方法经常被标记为非public
是有原因的。通常,不应忽视作者的意图,但有时我们就是必须去做。
PolyStar
如果您可以控制形状的源代码,我们只需要公开我们创建的定义几何图形。
public Geometry CreateGeometry()
{
return DefiningGeometry;
}
正如您所见,这几乎和它能得到的一样简单。StackedGeometry
类负责缓存,因此它不是很大的损失,因为它不在PolyStar
中。
文本
现在,除了它的含义之外,一段文本就是一组曲线。幸运的是,框架使我们可以轻松访问底层几何图形。文本信息存储在TextBlock
中以方便使用。
public System.Windows.Media.Geometry CreateGeometry()
{
ICollection<typeface> faces = mTextBlock.FontFamily.GetTypefaces();
FormattedText fText
= new FormattedText(mTextBlock.Text, CultureInfo.CurrentCulture,
FlowDirection.LeftToRight, faces.First(),
mTextBlock.FontSize, Brushes.Black);
mTextGeometry = fText.BuildGeometry(new Point(0, 0));
return mTextGeometry;
}
我在这里选择了简单的。这个应用程序远非制作出严肃的文本效果。PhotoshopRoadmap上有一些非常好的效果。Chrome和Radioactive文本效果很棒,但这些需要自定义着色器效果和一些花哨的几何计算。
现在,我们可能会注意到这里存在一个潜在的问题。假设您将此样式化文本的一部分用作应用程序品牌推广的一部分。毕竟,这就是它的目的。我们不希望每个客户端都必须安装适当的字体。这就是为什么IFactory
接口(IGeometrySource
实现)具有IsFrozen
属性的原因。当StackedGeometry
类使用Geometry Source时,它会检查是否应使用CreateGeometry
。
if (!GeometrySource.IsFrozen | GeometrySource.DesignMode)
{
mGeometry = GeometrySource.CreateGeometry();
GeometrySource.Geometry = mGeometry;
}
else
{
mGeometry = GeometrySource.Geometry;
}
我们允许使用存储在XAML中的几何图形。请注意,当表示为XAML时,此几何图形可能会很大。
IEffectFactory
既然我们有了几何图形,我们需要对它们做些事情。IEffectFactory
接口与IGeometrySource
接口非常相似。
[CustomXaml]
public interface IEffectFactory:IFactory
{
ShapeVisualPropsCollection CreateLayerCollection();
ShapeVisualPropsCollection ShapeVisualProps { get; set; }
}
Effect Factories通过CreateLayerCollection
生成视觉属性集合,并通过ShapeVisualProps
属性进行缓存。创建ShapeVisualPropsCollection
类是为了让XamlWriter
不会因为泛型而崩溃。
public class ShapeVisualPropsCollection:ObservableCollection<shapevisualprops>{}
Shape
视觉ShapeVisualProps
类包含Shape
类中的所有视觉属性。由于我希望该类像Shape
一样运行,所以我只是从Reflector中复制了代码并做了一些小的调整。我强烈推荐这样做。如果您想模仿框架中的某些内容,值得考虑。ShapeVisualProps
有一个重要的方法Apply
,它将其属性应用于所讨论的形状。
public void Apply(Shape shape)
{
shape.Fill = Fill;
shape.StrokeDashArray = StrokeDashArray;
shape.StrokeDashCap = StrokeDashCap;
shape.StrokeDashOffset = StrokeDashOffset;
shape.StrokeEndLineCap = StrokeEndLineCap;
shape.StrokeLineJoin = StrokeLineJoin;
shape.StrokeMiterLimit = StrokeMiterLimit;
shape.Stroke = Stroke;
shape.StrokeStartLineCap = StrokeStartLineCap;
shape.StrokeThickness = StrokeThickness;
shape.Effect = mEffect;
shape.RenderTransform = mTransformGroup;
}
到目前为止,此解决方案中已创建了两个工厂:NeonFactory
和ComicFactory
。
霓虹灯工厂
霓虹灯由充有氖气或其他惰性气体的玻璃管组成。光线有三个主要区域:内部发光的辉光柱,没有气体的玻璃边缘,以及周围的发光。
public ShapeVisualPropsCollection CreateLayerCollection()
{
ShapeVisualPropsCollection svps = new ShapeVisualPropsCollection();
ShapeVisualProps svp = new ShapeVisualProps();
double currentSaturation = StartingSaturation;
double currentBrightness = StartingBrightness;
//Glow
svp.Stroke = new SolidColorBrush(MediaColor(
DevCorpColor.ColorSpaceHelper.HSBtoColor(Hue,
currentSaturation, currentBrightness)));
svp.StrokeThickness = StartingThickness * 4 * GlowMultiplier;
svp.StrokeLineJoin = PenLineJoin.Round;
System.Windows.Media.Effects.BlurEffect blur =
new System.Windows.Media.Effects.BlurEffect();
blur.Radius = 12;
svp.Effect=blur ;
svps.Add(svp);
//glass edge
svp = new ShapeVisualProps();
currentSaturation *= SaurationMultiplier[0];
currentBrightness *= BrightnessMultiplier[0];
svp.Stroke = new SolidColorBrush(MediaColor(
DevCorpColor.ColorSpaceHelper.HSBtoColor(Hue, currentSaturation,
currentBrightness)));
svp.StrokeThickness = StartingThickness * 2;
svp.StrokeLineJoin = PenLineJoin.Round;
svps.Add(svp);
// Gas Column
svp = new ShapeVisualProps();
currentSaturation *= SaurationMultiplier[1];
currentBrightness *= BrightnessMultiplier[1];
svp.Stroke = new SolidColorBrush(MediaColor(
DevCorpColor.ColorSpaceHelper.HSBtoColor(Hue, currentSaturation,
currentBrightness)));
svp.StrokeThickness = StartingThickness ;
svp.StrokeLineJoin = PenLineJoin.Round;
svps.Add(svp);
return svps;
}
气体层最亮,然后是玻璃层,最后是漫射的光辉。特别感谢Guillaume Leparmentier的项目:在.NET中操作颜色 - 第1部分。他的一些颜色处理代码为我节省了大量时间。霓虹灯的饱和度较低,并且当我们从光柱到辉光时,色相和饱和度保持不变,但亮度会变化。只需很少的努力,我们就可以制作出看起来像这样的东西。
漫画工厂
我在网上发现了这种风格,觉得它很有趣。概念很简单。
- 三色渐变
- 渐变周围有深色边框
- 底部渐变色的附加边框
- 中间渐变色的最终边框
创建此效果的代码非常简单。
public ShapeVisualPropsCollection CreateLayerCollection()
{
ShapeVisualPropsCollection svps = new ShapeVisualPropsCollection();
ShapeVisualProps svp;
//Third Outline
svp = new ShapeVisualProps();
svp.Stroke = new SolidColorBrush(MiddleColor);
svp.StrokeThickness = 11;
svps.Add(svp);
//Second Outline
svp = new ShapeVisualProps();
svp.Stroke = new SolidColorBrush(BottomColor);
svp.StrokeThickness = 7;
svps.Add(svp);
//First Outline
svp = new ShapeVisualProps();
svp.Stroke = new SolidColorBrush(OutlineColor);
svp.StrokeThickness = 3 ;
svps.Add(svp);
//Gradient Layer
svp = new ShapeVisualProps();
LinearGradientBrush lgb = new LinearGradientBrush();
lgb.GradientStops.Add(new GradientStop(BottomColor, 0));
lgb.GradientStops.Add(new GradientStop(MiddleColor, .5));
lgb.GradientStops.Add(new GradientStop(TopColor, 1));
lgb.StartPoint = new System.Windows.Point(0, 1);
lgb.EndPoint = new System.Windows.Point(0, 0);
svp.Fill = lgb;
svps.Add(svp);
return svps;
}
尽管它最初是为文本设计的,但我对它在形状上的表现感到惊讶。
特别感谢Microsoft提供的ColorPicker Custom Control Sample,该控件用于为漫画工厂选择颜色。
整合它们
StackedGeometry
类管理IEffectFactory
和IGeometrySource
。此类的主要方法是PrepareShapeStack
方法。
private void PrepareShapeStack()
{
if (GeometrySource != null )
{
if (!GeometrySource.IsFrozen | GeometrySource.DesignMode)
{
mGeometry = GeometrySource.CreateGeometry();
GeometrySource.Geometry = mGeometry;
}
else
{
mGeometry = GeometrySource.Geometry;
}
}
if (EffectFactory != null)
{
if (!EffectFactory.IsFrozen | EffectFactory.DesignMode)
{
mShapeVisualPropsCollection =
EffectFactory.CreateLayerCollection();
EffectFactory.ShapeVisualProps = mShapeVisualPropsCollection;
}
else
{
mShapeVisualPropsCollection = EffectFactory.ShapeVisualProps;
}
}
if (mGeometry != null && mShapeVisualPropsCollection != null)
{
foreach (ShapeVisualProps layer in mShapeVisualPropsCollection)
{
Path ext = new Path() { Data = mGeometry };
layer.Apply(ext);
this.Children.Add(ext);
}
}
}
这就是外观组合的地方。DesignMode
属性由应用程序使用,以便我们可以使应用程序冻结并生成属性以生成所需的XAML。StackedGeometryBrushFactory
公开一个VisualBrush
,其视觉效果是StackedGeometry
。
插件架构
该解决方案包含八个程序集:StackedGeometryDesign
、StackedGeometry
、GeometrySources
、EffectFactories
、PolygonImageLib
、PointTransformations
、ColorPicker
和DevcorpColor
。依赖关系图可能很有帮助。(箭头指向依赖方向。)
除支持库外,所有内容都依赖于StackedGeometry
,其中包含所有使用的接口以及自定义属性。StackedGeometryDesign
和StackedGeometryAssemblies
对其他程序集一无所知。它们依赖于StackedGeometry
中定义的属性和接口。定义了以下属性:
ContainsEffectFactoriesAttribute
- 用于将程序集标记为包含IEffectFactory
类型。ContainsGeometrySourcesAttribute
- 用于将程序集标记为包含IGeometrySource
类型。EffectFactoryAttribute
- 用于将类标记为IEffectFactory
。GeometrySourceAttribute
- 用于将类标记为IGeometrySource
。CustomXamlAttribute
- 用于将类或接口标记为具有自定义XAML。XamlIgnoreAttribute
- 用于将属性标记为在XAML目的上始终被忽略或有条件地被忽略。
加载中
StackedGeometryDesign
应用程序搜索其当前目录和子目录中的类库,然后检查它们是否具有ContainsEffectFactoriesAttribute
或ContainsGeometrySourcesAttribute
属性。如果是,则检查类型是否具有EffectFactoryAttribute
和GeometrySourceAttribute
属性。然后实例化找到的类。代码编写完毕后,又以更优化的方式重写。最初,代码看起来像这样:
string[] files = Directory.GetFiles(currentAssemblyDirectoryName,
"*.dll", SearchOption.AllDirectories);
foreach (string str in files)
{
Assembly asm = Assembly.LoadFile(str);
ContainsGeometrySourcesAttribute containsGeom =
(ContainsGeometrySourcesAttribute)Attribute.GetCustomAttribute(asm,
typeof(ContainsGeometrySourcesAttribute));
f (containsGeom != null) //Attribute null if not present
{
foreach (Type t in asm.GetTypes())
{
GeometrySourceAttribute gsa = (GeometrySourceAttribute)
Attribute.GetCustomAttribute(t, typeof(GeometrySourceAttribute));
if (gsa != null)
//Attribute null if not present
{
IGeometrySource gs =
(IGeometrySource)Activator.CreateInstance(t, null);
geometrySources.Add(gs);
}
}
}
此方法有效,但当程序集同时具有这两个属性时效率不高。为这种情况创建了一个更通用的解决方案,由AttributeReflectionItem
和AttributeReflector
组成。AttributeReflectionItem
仅包含属性搜索的三个数据项。
class AttributeReflectionItem
{
public Type AssemblyAttribute { get; set; }
public Type ClassAttribute { get; set; }
public IList List { get; set; }
}
AttributeReflector
逐个遍历程序集并填充相应的列表。
public void Reflect(Assembly assembly)
{
List<attributereflectionitem> assemblyReflectionItems =
new List<attributereflectionitem>();
foreach (AttributeReflectionItem ri in mReflectionItems)
{
if (Attribute.GetCustomAttribute(assembly,
ri.AssemblyAttribute) != null)
{
assemblyReflectionItems.Add(ri);
}
}
if (mReflectionItems.Count > 0)
{
foreach (Type t in assembly.GetTypes()) // only Called once
{
foreach (AttributeReflectionItem ri in mReflectionItems)
{
if (Attribute.GetCustomAttribute(t, ri.ClassAttribute) != null)
{
ri.List.Add(Activator.CreateInstance(t, null));
}
}
}
}
}
由于反射调用被限制在最低限度,因此此方法将效率更高,但代价是更高的抽象。此外,添加其他搜索属性也变得微不足道。
ar.ReflectionItems.Add(new AttributeReflectionItem()
{
AssemblyAttribute = typeof(ContainsGeometrySourcesAttribute),
ClassAttribute = typeof(GeometrySourceAttribute),
List = geometrySources
});
交互
已创建的类通过它们的接口或通过UI进行交互。WPF提供了多种方式使类在用户界面中可见。我们可以使用DataTemplate
或制作自定义控件。我选择了一种略有不同的方式。类具有作为属性的DataTemplate
。
public DataTemplate DataTemplate
{
get
{
ResourceDictionary rd = new ResourceDictionary();
rd.Source = new
Uri("EffectFactories;component/NeonResources.xaml",
UriKind.Relative);
DataTemplate dt = (DataTemplate)rd["NeonFactoryTemplate"];
return dt;
}
}
这些DataTemplate
从类所在的程序集中的资源字典加载。然后,可以将类放置在ContentControl
中,并将ContentTemplate
设置为IEffectFactory
或IGeometrySource
的DataTemplate
属性。
<ContentControl Name="cEffects" VerticalAlignment="Top"
Content="{Binding Source ={
StaticResource StackedGeometry} ,
Path=EffectFactory}"
ContentTemplate="{Binding Source ={
StaticResource StackedGeometry} ,
Path=EffectFactory.DataTemplate }"/>
这样,托管应用程序就不需要了解包含类和资源字典的程序集的任何信息。在开发过程中,这些DataTemplate
可以保留在主程序集中以便于编辑,然后稍后移动。
XAML文档
StackedGeometryDesign
应用程序旨在实用。我们需要能够轻松地将这些StackedGeometryBrushFactory
对象放入他们的应用程序中。为此,该应用程序生成StackedGeometryBrushFactory
的XAML。通过调用可以非常容易地获得XAML。
string xamlString = XamlWriter.Save(mUIElement);
但是,生成的XAML过于冗长,以至于无法使用。由于使用属性控制序列化似乎是Windows的传统,所以我决定走这条路。XamlGenerator
类完成了大部分工作。基本策略如下:
- 使用
XamlWriter
创建XAML。 - 递归地遍历对象层次结构,查找具有
CustomXamlAttribute
的对象。 - 在这些对象中,检查
XamlIgnoreAttribute
,并可能从XAML中删除该对象的序列化。
缓存
缓存对于提高基于反射的代码的性能非常有效。检查对象哪些属性可能需要从XAML中删除。
Dictionary<type,> mSpecialInfoCache = new Dictionary<type,>(); //For Caching
private List<propertyinfo> SpecialXamlInfos(Type t)
{
//Caching speed improvement ~1000 times
if (mSpecialInfoCache.ContainsKey(t))
{
return mSpecialInfoCache[t];
}
else
{
PropertyInfo[] infos =t.GetProperties(BindingFlags.Public |
BindingFlags.Instance);
List<propertyinfo> specialInfos = new List<propertyinfo>();
foreach (PropertyInfo pi in infos)
{
if (HasCustomXaml(pi.PropertyType) | GetXamlIgnoreStatus(pi)!=
eXamlIgnoreStatus.noAttribute)
{
specialInfos.Add(pi);
}
}
mSpecialInfoCache.Add(t, specialInfos);
return specialInfos;
}
}
我的机器上,第一次运行后,此代码的运行速度大约是原来的千倍。部分原因是控件有70多个依赖项属性,但即便如此,这也是一个惊人的差异。类似以下的代码仅加速了约三十倍。
Dictionary<type,> mHasCustomXaml = new Dictionary<type,>();//For Caching
private bool HasCustomXaml(Type t )
{
//Caching speed improvement ~30 times
if (mHasCustomXaml.ContainsKey(t))
{
return mHasCustomXaml[t];
}
else
{
CustomXamlAttribute cxa = (
CustomXamlAttribute)Attribute.GetCustomAttribute(t,
typeof(CustomXamlAttribute));
mHasCustomXaml.Add(t, cxa != null);
return (cxa != null);
}
}
Type
和PropertyInfo
类是全局唯一的(至少在AppDomain内),因此使用它们作为字典键应该没有困难。
生成代码
XamlGenerator
的GenerateXaml
方法完成了XAML创建的所有繁重工作。关于XamlWriter
,有几件事需要牢记。首先,它会将所有可能需要的命名空间放在第一个元素中。这没什么错,但这意味着表示对象子元素的字符串默认情况下与XamlWriter
从该元素生成的字符串不同。其次,元素可以以三种不同的方式表示。前两种是XML的标准方式。属性可以表示为属性或子元素。这些可以通过正则表达式轻松提取。但是,对象可以具有**ContentPropertyAttribute
**。这使得它们看起来没有标签,因此更难提取。我们需要为对象创建XAML,删除命名空间,然后找到它以进行删除或自定义。最后,在此应用程序中,只有当IsFrozen
属性为true
时,才需要将Geometry
和ShapeVisualProps
写入XAML。如果您有兴趣,请查看代码。该方法很长,远不漂亮,但同样,框架在此情况下使用的一些代码也很有挑战性,例如System.Windows.Markup.Primitives.MarkupWriter.WriteItem
。
结论
感谢您耐心阅读所有这些内容。最初只是一个简单的霓虹灯星,最终发展成完全不同于最初设想的东西。使其易于扩展成为当务之急,因为我打算在不久的将来创建一些其他几何图形,并且不希望担心集成问题。特别是,PolyArc和一些分形几何图形即将到来。一旦我找到一种制作足够令人愉悦的浅银色效果的方法,我将看看有多少可以转移到Silverlight,敬请关注。
更新
- 2010年7月15日 - 对演示应用程序进行了一些优化。