Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Stacked Geometry Brush Factory

0.00/5 (No votes)
16 Jul 2010 1  
A geometric visual brush producer with plug-in architecture and customized XAML.

WPF text iam

Introduction

This article is about creating effects with shapes, and additionally, how to build a plug-in architecture for WPF as well as control XAML serialization through attributes. It began as a glowing neon star for use as an icon, and ended as a versatile visual brush factory. It soon became obvious that the solution could be extended. I decided to create an application for creating graphical effects with the following characteristics:

  • Able to use a number of different sources of geometry (Geometry Sources) and styles (Effect Factories) in a mix and match fashion.
  • Geometry Sources and Effect Factories should be able to be added at a later date without my needing to alter existing code.
  • This was not just to be a pretty image generator. It would need to produce reasonably concise XAML that I could paste into another project and have the same images.

Most of the time, we create effects in Illustrator or Photoshop by taking an image and then making copies and stacking them on top of each other. Each layer is then transformed in some way. Some rather striking effects can be made rather easily in this way. The following is the basic plan for generating stacked shapes:

  • A geometry is created from a Geometry Source class.
  • A collection of effects is created from an Effect Factory.
  • The geometry is used to create a collection of Paths.
  • The effects are applied to the Paths.
  • The Paths are added in order as Children of a Canvas.
  • A visual brush is created from this Canvas and we are done.

Background

It might be useful, and potentially entertaining, to go to one of the Internet graphics training sites like TutVid.com and watch some of the videos. In particular, the comic Effect Factory was inspired by one of their videos. If you search YouTube under Illustrator tutorial, you will also find lots of excellent info.

It is also worth noting that this article originally came about as a means of improving the appearance of PolyStars. If you want a more detailed explanation of the polystar geometry, you can find it there.

Creating Geometries

The effects will be made by making identical copies of a shape and then modifying them. The following method will copy most shapes, and even though it was rejected, it is worth looking at for a moment. The only restriction is not to directly expose generic types as that will cause the XamlWriter to fail.

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;
}

Originally, a method very similar to this one was used in this solution. It worked, but was unsatisfactory. Using serialization seems like overkill for this situation.

Among the classes that the framework provides that inherit from the shape class, the Path is noteworthy in that it lets us insert its geometry. Fortunately, there is no problem with multiple Path instances sharing the same geometry. The issue is how we create the geometry, of which there are three main cases:

  • We have control of the class so that we can expose the protected geometry.
  • The source of the geometry is a shape.
  • The source of the geometry is some text.

The IGeometrySource

The IGeometrySource interface was created to provide a source of Geometry for instances of ExternalGeometryShape.

[CustomXaml]
public interface IGeometrySource:IFactory 
{
   //Main Method analogus to CacheDefiningGeometry
   Geometry CreateGeometry();
   // Supplies geometry Analogus to GetDefiningGeometry
   Geometry Geometry { get; set; }
}

The CreateGeometry method is enough to ensure that the class implementing the IGeometrySource interface can be used to provide Geometry objects while the Geometry property is used by StackedGeometry for caching. I have the option of modifying the class to support this interface as I did with the PolyStar, or create a wrapper class that implements the interface as with the TextBlock and the Shape. The IFactory interface is used for the plug-in system as well as for updating the display.

Shapes

All classes inheriting from Shape implement the protected DefiningGeometry method. Our first instinct would probably be to inherit from Shape and then expose the method. Unfortunately, all of the framework classes that inherit from Shape are sealed. We can always call any class through Reflection if we really want to, regardless of how they are exposed.

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);
}

Let's take a look at the methods from the Polygon class (Thanks Reflector):

WPF text iam

Almost all of the work is done in the CacheDefiningGeometry method. If the shape in question is embedded in XAML, there is a very good chance that it has been rendered prior to our attempt to call its geometry, but it's safer to call the CacheDefiningGeometry method first.

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 { }
}

This is the code that is used to take the user's input and create a shape from it. It is worth noting that if we call the DefiningGeometry property without calling the CacheDefiningGeometry method, it will always be null. The mistake is unfortunately very easy to make. The above code will work; however, it is not very fast as it could be, since the GetProperty and GetMethod methods are slow. The use for Reflection that we usually hear advertised is to allow the use of assemblies and members not known at compile time, but in this case, we already know the members we want to invoke. They just happen to be other than public, so let's do everything with statics. Consider this code:

[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);
       }
   }
   //..
}

This code should run at speeds comparable to that of a normal method as the slow methods only need to be called once. At this juncture, I feel obligated to state that methods are often marked as other than public for a reason. In general, it is not a good idea to disregard an author's intention, but then again, there are times when we just have to go for it.

PolyStar

If you have control over the source code for the shape, all we need to do is expose the defining geometry that we had to create.

public Geometry CreateGeometry()
{
    return DefiningGeometry;
}

As you can see, this is about as trivial as it can get. The StackedGeometry class handles caching, so it's not much of a loss that it is not in the PolyStar.

Text

Now, aside from its meaning, a block of text is just a set of curves. Fortunately, the framework provides us with easy access to the underlying geometry. The text information is stored within a TextBlock for convenience.

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;
}

I went for simple here. It takes a lot more than this application to make serious text effects. PhotoshopRoadmap has some really good ones. The Chrome and Radioactive text are awesome, but these would require custom shader effects and some fancy geometrical calculations.

Now, we might note that there is a potential problem here. Let's assume that you use some of this styled text as part of the branding for your app. After all, that is what it is intended for. We do not want it to be necessary for every client to have the appropriate fonts installed. That's why the IFactory interface which IGeometrySource implements has the IsFrozen property. When the StackedGeometry class uses the Geometry Source, it checks to see if it should use CreateGeometry.

if (!GeometrySource.IsFrozen | GeometrySource.DesignMode)
{
   mGeometry = GeometrySource.CreateGeometry();
   GeometrySource.Geometry = mGeometry;
}
else
{
   mGeometry = GeometrySource.Geometry;
}

We are allowed to use a geometry that is stored in the XAML instead. Be forewarned that this geometry can easily be large when expressed as XAML.

The IEffectfactory

Now that we have our geometries, we need to do something with them. The IEffectFactory interface is very similar to the IGeometrySource one.

[CustomXaml]
public interface IEffectFactory:IFactory 
{
    ShapeVisualPropsCollection CreateLayerCollection();
    ShapeVisualPropsCollection ShapeVisualProps { get; set; }
}

Effect Factories produce collections of visual properties through CreateLayerCollection, and are cached through the ShapeVisualProps property. The ShapeVisualPropsCollection class was created so that the XamlWriter would not choke on Generics.

public class ShapeVisualPropsCollection:ObservableCollection<shapevisualprops>{}

The Shape visual ShapeVisualProps class contains all of the visual properties in the Shape class. As I wanted the class to behave exactly like a Shape, I simply copied the code out of Reflector and made some minor adjustments. This is something I strongly recommend. If you want to imitate something in the framework, it is worth considering. ShapeVisualProps has a significant method Apply which applies its properties to the shape in question.

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;
}

There are two factories created for this solution so far, the NeonFactory and the ComicFactory.

Neon Factory

A Neon light consists of a glass tube filled with neon or other noble gasses. The light has three main areas: the inner glowing column of gas, the edge of the glass where there is no gas, and the surrounding glow.

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;
}

The gas layer is the brightest, followed by a step down to the glass layer, and finally, there is a diffuse glow. A special thanks should go to Guillaume Leparmentier for his project: Manipulating colors in .NET - Part 1. Some of his color manipulation code saved me considerable time. Neon is lightly saturated, and as we go from the column to the glow, the hue and saturation are unchanged, but the brightness varies. With very little effort, we can make things that look like this:

WPF text iam

Comic Factory

I discovered this style on the internet and thought that it would be interesting. The concept is simple.

  • A three color gradient
  • A dark border around the gradient
  • An additional border for the bottom gradient color
  • A final border for the middle gradient color

The code to create this effect is straightforward:

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;
}

Even though it was intended for text, I was surprised how well it came out with shapes.

WPF text iam

Special thanks to Microsoft for their ColorPicker Custom Control Sample which was used to select colors for the comic factory.

Putting Them Together

The StackedGeometry class manages the IEffectFactory and the IGeometrySource. The main method of this class is the PrepareShapeStack method.

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);
        }
    }
}

This is where the look is put together. The DesignMode properties are used by the application to make it so that we can both have the application frozen and also generate the properties so as to generate the desired XAML. The StackedGeometryBrushFactory exposes a VisualBrush whose visual is the StackedGeometry.

Plug-in Architecture

The solution has eight assemblies in it: StackedGeometryDesign, StackedGeometry, GeometrySources, EffectFactories, PolygonImageLib, PointTransformations, ColorPicker, and DevcorpColor. A dependency diagram might be helpful. (Arrays point in the direction of dependency.)

WPF text iam

Everything except the support libraries depend on StackedGeometry, which contains all of the interfaces used as well as the custom attributes. Neither the StackedGeometryDesign nor StackedGeometryAssemblies know anything about the other assemblies. They depend on attributes and interfaces defined in StackedGeometry. The following attributes are defined:

  • ContainsEffectFactoriesAttribute - Used to flag an assembly as containing the IEffectFactory types.
  • ContainsGeometrySourcesAttribute - Used to flag an assembly as containing the IGeometrySource types.
  • EffectFactoryAttribute - Used to flag a class as being an IEffectFactory.
  • GeometrySourceAttribute - Used to flag a class as being an IGeometrySource.
  • CustomXamlAttribute - Used to flag a class or interface as having customized XAML.
  • XamlIgnoreAttribute - Used to mark a property as either always being ignored for XAML purposes or conditionally ignored for XAML purposes.

Loading

The StackedGeometryDesign application searches its current directory and subdirectories for class libraries, then checks if they have the ContainsEffectFactoriesAttribute or ContainsGeometrySourcesAttribute attributes. If so, the types are checked for the EffectFactoryAttribute and GeometrySourceAttribute attributes. The found classes are then instantiated. The code was written and then rewritten in a more optimized way. Originally, the code looked something like this:

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);
            }
        }
    }

This method works, but is not so efficient when assemblies have both attributes. A more general solution was made for such cases, consisting of AttributeReflectionItems and an AttributeReflector. The AttributeReflectionItem just holds the three pieces of data for the attribute search.

class AttributeReflectionItem
{
    public Type AssemblyAttribute { get; set; }
    public Type ClassAttribute { get; set; }
    public IList List { get; set; }
}

The AttributeReflector goes through the assemblies one by one and fills the appropriate lists.

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));
                }
            }
        }
    }
}

Since the Reflection calls are kept to a minimum, this one will be a bit more efficient at the cost of greater abstraction. Also, it becomes trivial to add additional search attributes.

ar.ReflectionItems.Add(new AttributeReflectionItem()
{
    AssemblyAttribute = typeof(ContainsGeometrySourcesAttribute),
    ClassAttribute = typeof(GeometrySourceAttribute),
    List = geometrySources
});

Interacting

The classes that have been created interact either through their interfaces or through the UI. WPF provides multiple ways for classes to be visible in the user interface. We can use DataTemplates or make our classes Custom Controls. I chose something a little different. The classes have DataTemplates as properties.

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;
    }
}

These DataTemplates are loaded from resource dictionaries in the same assemblies as the classes. The classes can then be placed in ContentControls with the ContentTemplate being set to the DataTemplate property of the IEffectFactory or IGeometrySource.

<ContentControl Name="cEffects" VerticalAlignment="Top" 
                Content="{Binding Source ={
                          StaticResource StackedGeometry} ,
                          Path=EffectFactory}"
                          ContentTemplate="{Binding Source ={
                          StaticResource StackedGeometry} , 
                          Path=EffectFactory.DataTemplate }"/>

This way, the hosting application does not need to know anything about the assembly that contains the classes and resource dictionaries. While under construction, these DataTemplates can be kept in the main assembly for easy editing and then moved later.

XAML Documentation

The StackedGeometryDesign application is meant to be a practical one. It needs to be easy for us to place these StackedGeometryBrushFactory objects in their applications. To that end, the application produces the XAML for the StackedGeometryBrushFactory. One can get XAML very easily by calling:

string xamlString = XamlWriter.Save(mUIElement);

However, the produced XAML is verbose to the point of being unusable. As it seems to be a Windows tradition to control serialization with attributes, I decided to go that route. The XamlGenerator class does most of the work. The basic strategy is as follows:

  • Create the XAML using the XamlWriter.
  • Go through the object hierarchy recursively, looking for objects with the CustomXamlAttribute.
  • Within these objects, check for the XamlIgnoreAttribute and possibly remove that object's serialization from the XAML.

Caching

Caching is remarkably effective at improving the performance of Reflection-based code. The objects are checked for which of their properties may need to be removed from the 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;
    }
}

This code runs at about one thousand times faster on my machine after the first time through. Part of the reason is that controls have 70 plus dependency properties, but even so, this is an amazing difference. Code like the following is only accelerated by a factor of thirty:

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);
    }
}

The Type and PropertyInfo classes are globally unique, at least within an AppDomain, so there should be no difficulty in using them for dictionary keys.

Generation Code

The GenerateXaml method of the XamlGenerator does all the heavy lifting for XAML creation. There are a few things we need to keep in mind concerning the XamlWriter. First, it will place all of the potentially needed namespaces in the first element. There is nothing wrong with this, but it means that the string representing a sub-element of an object is not, by default, the same as what the XamlWriter would produce from that element. Secondly, elements can be represented in three distinct ways. The first two are the standard ones from XML. The property can be represented as either an attribute or a sub-element. Those can be easily extracted via Regular Expressions. However, objects can have the ContentPropertyAttribute. This makes them appear unlabeled and thus more difficult to extract. We need to create the XAML for the object, remove the namespaces, and then find it in the string to remove or customize. Finally, in this application, the Geometry and ShapeVisualProps only need to be written out to XAML if the IsFrozen property is true. If you are interested, please look at the code. The method is long and far from beautiful, but then again, some of the code the framework uses in this case is also challenging, System.Windows.Markup.Primitives.MarkupWriter.WriteItem.

Conclusion

Thanks for your patience in reading all of this. What began as a simple Neon star grew to something completely different than originally envisioned. Making this easily extensible became a priority as I intend to create some other geometries in the near future and do not want to worry about integration. In particular, a PolyArc and some Fractal Geometries are on the way. As soon as I figure out a way to make a sufficiently pleasant Light Silver effect, I will see how much of this can go over to Silverlight, so stay tuned.

Updates

  • 7/15/2010- Some optimization was made to the demo application.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here