Introduction
This article is comprised of a Shape
class for generating polygons and stars as well as a designer that enables us to preview these objects. This started when I found myself in need of a “Spinny Thingy” to display during a long process. A geometric shape evolved, and a designer application along with it.
Most of this article will cover the implementing of the geometrical transformations to create the stars and polygons (Polystars, henceforth). Along the way, I ran afoul of some oddities in the implementation of the DependencyProperty
class. Hopefully, my experience can be of help.
Although understanding is a great and wonderful thing, the PolyStar
class and the design application can be used prior to examining their inner workings. You can put a reference to PolygonImageLib
in you project, construct the polygon with the Generator tool, and then get the XAML and paste it in, either as a PolyStar
or as a Polygon
.
Assemblies
The code consists of three assemblies. PolygonBuilder contains the code for displaying the PolyStar
s, PolygonImageLib contains the classes for the PolyStar
, and PointTransformations contains the basic classes for manipulating the points in the polygons. The classes in PointTransformations
have mostly come from my last article.
Polygons and Stars
Polygons can be thought of as an equiangular set of points along a circle specified by a radius and a number of points/sides. The code to generate both stars and polygons is in PolyMatrixBuilder
.
if (mShapeType == EShapeType.Polygon)
{
Matrix mat = new Matrix(2);
for (int i = 0; i < mSides; i++)
{
double x = mRadius * Math.Cos((2 * Math.PI / mSides) * i);
double y = mRadius * Math.Sin((2 * Math.PI / mSides) * i);
mat.AddRow(new Double[] { x, y });
}
Stars are a bit more complicated. One of the ways of defining a star is to think of it as two polygons with the same number of sides and the same origin but different radii, but with the inner one rotated.
To create the star, we clones the matrix of points, rotate them, and interleave the two matrices. Note that the inner radius can be negative, which makes an interesting effect, especially when the negative inner radius number is just a little smaller, absolute value wise, than the radius. The interface is set up so that negative values need to be typed rather than come from the slider.
Angular Offset
The angular offset can be thought of as the amount that the inner polygon is rotated away from where it would be in a normal star.
Note that it can be either positive or negative. Also, if the offset is greater than 360/(number of sides) in degrees, the geometry changes significantly.
Bevel
A Bevel is usually thought of as lopping off the corners of a shape. Technically, I should speak of chamfers, but that is not common usage. There is another way to think of a bevel. We could make a copy of the set of points, rotate it slightly, and then interleave the two sets of points. This is the approach that I took.
We get interesting effects when the bevel angle is negative, or when the angle is larger than 360/(number of sides) degrees for a polygon, or 180/(number of sides) degrees for a star.
Star Generation Code
Combining all of the geometrical notions in the code, we get the following for a star:
Matrix innerMat = (Matrix)outerMat.Clone();
innerMat.Multiply(mInnerRadius / mRadius);
PointTransformations.RotationalTransformation rot =
new RotationalTransformation() { Matrix = innerMat };
rot.RotateRadiansAroundOrigin(Math.PI / mSides);
if (mIsAngularlyOffset)
{
rot.RotateDegreesAroundOrigin(mAngularOffset);
}
outerMat.Interleave(innerMat, false);
if (mIsBeveled )
{
Matrix bevelmat = (Matrix)outerMat.Clone();
rot.Matrix = bevelmat;
rot.RotateDegreesAroundOrigin(mBevelOffset);
outerMat.Interleave(bevelmat, false);
rot.Matrix = outerMat;
rot.RotateDegreesAroundOrigin(-mBevelOffset / 2);
}
After this, invoke some overall transformations contained in OverallTransformation
s to rotate the shape, or flatten it in either the vertical or horizontal directions. This was done to ease maintenance.
Dependency Properties
My motivation for creating the PolyStar
was to animate it. That requires that most of the properties be dependency properties. All went well with a few minor irritations. Please note that if you have a double
property, you need to set the default to 0.0 rather than 0 for the value of zero. It will not cast it for you, and will also crash in the XAML where the object is created, rather than at the point of the faulty assignment.
Problems occurred when I decided that it would be better if setting IsRotated
to false
would also set the value of Rotation
to 0. Naively, I thought the following code would work:
set
{
SetValue(IsRotatedProperty, value);
Rotation= 0;
}
The Rotation
is not changed, but IRotated
is modified just fine. After a while, I tried the following:
set
{
double[] x = { 0 };
double y = x[4];
SetValue(IsRotatedProperty, value);
double z = x[4];
}
This should generate a runtime error, but does not. Apparently, the compiler just ignores everything in the set
statement, except SetValue
. However, if there is a syntax error, it will fail to compile. If you want to do something when the property is set, you will need to use a callback. Putting code in the set
area will not work.
Here is a version of the property that works.
Static Helper Functions
private static DependencyProperty DependencyProp
(string name, System.Type type,object defaultValue){
FrameworkPropertyMetadata fpm = new FrameworkPropertyMetadata
( defaultValue, FrameworkPropertyMetadataOptions.AffectsRender);
DependencyProperty dp = DependencyProperty.Register
(name, type , typeof(PolyStar), fpm);
return dp;
}
private static DependencyProperty DependencyProp
(string name, System.Type type, object defaultValue,
PropertyChangedCallback callback)
{
FrameworkPropertyMetadata fpm = new FrameworkPropertyMetadata
(defaultValue, FrameworkPropertyMetadataOptions.AffectsRender,callback);
DependencyProperty dp = DependencyProperty.Register
(name, type, typeof(PolyStar), fpm );
return dp;
}
Property Definition
public static DependencyProperty IsRotatedProperty =
DependencyProp("IsRotated", typeof(bool), false, IsRotatedCallBack);
public bool IsRotated
{
get { return (bool)GetValue(IsRotatedProperty ); }
set
{
SetValue(IsRotatedProperty, value);
}
}
And Finally, the Callback
static void IsRotatedCallBack(DependencyObject property,
DependencyPropertyChangedEventArgs args)
{
if (!(bool)args.NewValue)
{
PolyStar pTemp = (PolyStar)property;
pTemp.Rotation = 0;
}
}
Conclusion
There are other details about the technical implementation that might be interesting. IValueConverter<t>
is worth looking at. MainWindow.xaml uses the VisualBrush
in a productive way, something I had doubts that I would ever do. My purpose in writing this article was to make a tool so that people could easily add interesting polygons and stars to their applications. I hope to have succeeded.
History
- 27th September, 2008: Initial post