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

Spirograph Shapes: WPF Bezier Shapes from Math Formulae

0.00/5 (No votes)
29 Apr 2010 1  
A collection of shapes: Epicycloids, Epitrochoids, Hypocycloids, Hypotrochoids, Farris Wheels, Lissajous Curves, and Rose Curves.

Introduction

This article presents a group of shapes that you can use in your applications. Some of these, the Epitrochoids and Epicycloids, may be familiar if you had a Spirograph as a child. All of the shapes included here are defined by simple mathematical relations. The Farris Wheel is the only one that is even mildly complex. However, converting functions defined mathematically into vector visuals has until recently been rather laborious. The change was my last article (GraphDisplay: a Bezier based control for graphing functions and curves), in which I developed a procedure for the generation of PathGeometry elements that represented mathematically defined curves and functions. One use for such code is to make practical business or scientific applications. Another, the one chosen here, is to make it possible to include some pleasing mathematical vector shapes in applications with effectively no effort. In particular, absolutely no understanding of the math behind all of this is required to use these shapes in your applications. It is only for creating your own additional shapes that some math will be needed, but even then, understanding the algorithm behind all of this is completely optional.

Background

There is quite a bit of background one might find interesting, on both the internal workings of the shapes and the shapes themselves. If you want to understand how the application works, please see my article GraphDisplay: a Bezier based control for graphing functions and curves. For the shapes, the following references may he helpful:

There is also an article in Mathematics Magazine by F. A. Farris, "Wheels on Wheels on Wheels - Surprising Symmetry", Mathematics Magazine 69(3), 1996 pp. 185-189. Unfortunately, the article is not available online (for free at least), but can be easily found in almost any college library.

How to use it

The shapes can be used just as if they were any other shapes.

<f:Rose A="40" N="3" D="2" Stroke="Gray"  Fill="DarkSlateGray"/>

creates a Rose very similar to the one pictured above. You need to add the XML namespace for f.

xmlns:f="clr-namespace:FunctionalShapes;assembly=FunctionalShapes"

There is really not much more to say. The issue is that on may not know how to control the shape's appearance via the parameters. To that end, the demo application is a demonstration of the variety of possibilities inherent in the shapes.

The shapes

The main tool for experimenting with the various shapes is the Shape Explorer.

It shows the shape selected along with the formula used to calculate it. The various parameters can be adjusted, either through the sliders or the textboxes. You can also choose the fill stroke and background color.

Finally, you can adjust the number of Bezier segments used to construct the shape. SegmentAdjustment is the exponent of a power of 2 that multiplies the expected number of segments for the shape. So, if SegmentAdjustment is 0, then we have 2 to the 0 power, which is 1 or no change. For 3, we would have 8 times as many segments, and for -2, we would have 1/4 the number. Increasing the number makes the shape track the curve better at the cost of more calculations. (Note: Dramatically lowering the SegmentAdjustment can lead to unpredictable and at times beautiful curves.) This may seem strange, but the intention was to make the adjustment unbiased with respect to being larger or smaller. If I used straight multiplication, the slider would run from 0 to 16 and almost all of the space would be dedicated to increasing the number of segments. This illustrates the important point that there are often many ways you could choose to organize your parameters.

This is all good, but it does not give the full flavor of the shapes. To assist in this, each of the seven shapes has its own window that displays grids of shapes. These will be detailed along with their respective shapes.

Epicycloids

The Epicycloid is an old and famous curve. As far as we know, it was first discovered by Hipparchus of Rhodes (190-120 BC). It was put to very practical use in Astronomy through the long used Ptolemaic system. Thought of geometrically, a hypocycloid represents a circle rolling along the edge of another circle without slipping, which is equivalent to the following equations, with R1 being the radius of the base circle and R2 being the radius of the rolling circle.

This formula can be improved upon for geometric purposes. Instead of R1 and R2, we can use the ratio of R1/R2 and R2 to represent the Epicycloid. R1/R2 or k represents the shape, and R2 represents the size of the shape.

The Epicycloid curve will only repeat if k is rational. To ensure repeating shapes, I chose N (for Numerator) and D (for Denominator) in place of K and R as a scaling factor. I also added a phase shift P which has the effect of rotating the shape. This gives the following formula for the Hypocycloid that is used in the Hypocycloid class.

In code, this corresponds to:

int gcd = Mathematics.NumberTheory.GCD(N, D);//GCD for Greatest common Denominator
double p = (double)N / gcd;
double q = (double)D / gcd;
Double m = (double)p / q - 1;
Function fx = new Function(t => R * m * Math.Cos(t + P) + R * Math.Cos(m * t + P),
                           t => -R * m * Math.Sin(t + P) - m * R * Math.Sin(m * t + P));

Function fy = new Function(t => R * m * Math.Sin(t + P) - R * Math.Sin(m * t + P),
                           t => R * m * Math.Cos(t + P) - R * m * Math.Cos(m * t + P));
CyclicCurve c = new CyclicCurve(fx, fy,0,2* Math.PI * q);

Lots of details of how this code works can be found at GraphDisplay: a Bezier based control for graphing functions and curves, but the thing I would note here is that:

t => R * m * Math.Cos(t + P) + R * Math.Cos(m * t + P

corresponds to fx, while:

t => R * m * Math.Sin(t + P) - R * Math.Sin(m * t + P)

corresponds to fy. At this point, you might note that instead of a pair of functions fx and fy, you could think of this shape as being defined by a single complex function z(t), where fx is the real part of z and fy is the imaginary part of z(t).

Since we are defining the shape by a complete cycle of t, there is no reason why you cannot replace t by D*t, which gives the following interesting formulation.

Thus we can represent an Epicycloid by a sum of two complex exponentials with integral modifiers. The demo application also allows you to view grids of Epicycloids.

Epitrochoids

The Epitrochoid is an abstraction of the Epicycloid, which only took 1600 years to come about. Albrecht Dürer, of great artistic fame, wrote about these "spider lines" in 1525. Geometrically, an Epitrochoid defines the curve at a different radius than the rotating circle. It is the curve made in a spirograph when one of the circles is rotated on the outside of another circle. (Note that you can get pretty close to having an Epicycloid by using the hole at the very edge, but technically, it would need to be at the exact edge to get an Epicycloid rather than an Epitrochoid.) As far as the formula is concerned, the only difference is that the second R2 is replaced by an additional parameter D to represent the additional radius.

Just as with the Epicycloid, this formula can be improved upon for geometric purposes. Instead of R1 and R2, we can use the ratio of R1/R2 and R2 to represent the Epitrochoid. R1/R2 or k represents the shape, and R2 represents the size of the shape.

This shape uses the following version of the equation for an Epitrochoid. The parameter M is used in place of D so that when D=0 we have an Epitrochoid.

In code, this corresponds to:

int gcd = Mathematics.NumberTheory.GCD(N, D);
double p = (double)N / gcd;
double q = (double)D / gcd;

Double k = (double)p / q;
Function fx = new Function(t => R * ((k + 1) * Math.Cos(t + P) - 
                    Math.Pow(2, M) * Math.Cos((k + 1) * t + P)),
                  t => -R * ((k + 1) * Math.Sin(t + P) - (k + 1) * 
                    Math.Pow(2, M) * Math.Sin((k + 1) * t + P)));

Function fy = new Function(t => R * (k + 1) * Math.Sin(t + P) - R * 
                           Math.Pow(2, M) * Math.Sin((k + 1) * t + P),
                           t => R * (k + 1) * Math.Cos(t + P) - R * (k + 1) * 
                           Math.Pow(2, M) * Math.Cos((k + 1) * t + P));
CyclicCurve c = new CyclicCurve(fx, fy, 0, 2 * Math.PI * q);

We can see that:

t => R * ((k + 1) * Math.Cos(t + P) - Math.Pow(2, M) * Math.Cos((k + 1) * t + P))

corresponds to fx, while:

t => R * (k + 1) * Math.Sin(t + P) - R * Math.Pow(2, M) * Math.Sin((k + 1) * t + P)

corresponds to fy. Again, this can be represented by a single complex function z(t), where fx is the real part of z and fy is the imaginary part of z(t).

Not surprisingly, you can make the same substitution of t with D*t to get integer coefficients for the t variables.

The Epitrochoid display grid allows you to see a group of Epitrochoids together. Moving the M slider has a significant effect on their appearance.

Hypocycloids

Moving forward in time, Hypocycloids were first invented/discovered by Ole Romer (of speed of light fame) while studying gears in 1674. The hypocycloid can be thought of as the path formed by one circle rolling around within another circle. The following formula can be easily shown to represent a Hypocycloid:

It can then be written in terms of the ratio of R1 and R2.

The following version is the one used for the shape. Note that it has an extra parameter P for an overall phase shift (rotation) of the shape.

These equations are written in code as follows:

int gcd = Mathematics.NumberTheory.GCD(N, D);//GCD for Greatest common Denominator
double p = (double)N / gcd;
double q = (double)D / gcd;
Double m = (double)p / q - 1;
Function fx = new Function(t => R * m * Math.Cos(t + P) + R * Math.Cos(m * t + P),
                           t => -R * m * Math.Sin(t + P) - m * R * Math.Sin(m * t + P));

Function fy = new Function(t => R * m * Math.Sin(t + P) - R * Math.Sin(m * t + P),
                           t => R * m * Math.Cos(t + P) - R * m * Math.Cos(m * t + P));
CyclicCurve c = new CyclicCurve(fx, fy,0,2* Math.PI * q);

We can see that:

t => R * m * Math.Cos(t + P) + R * Math.Cos(m * t + P)

corresponds to fx, while:

t => R * m * Math.Sin(t + P) - R * Math.Sin(m * t + P)

corresponds to fy. Again, this can be represented by a single complex function z(t), where fx is the real part of z and fy is the imaginary part of z(t). Also, it is worth noting that cos(t) is an even function (cos(-t) = cos(t)) while sin(t) is odd (sin(-t) = -sin(t)).

If you substitute t with D*t, you get the integer coefficients for the t variables.

The Hypocycloid display grid allows you to see a group of Hypocycloids together.

Hypotrochoids

Hyptrochoids were first envisioned by Charles de Bovelles (Carolus Bovillus Latinized) in 1501. They are noted for being the curves created in a spirograph when a small wheel is rotated around inside a bigger ring. They can be represented mathematically by the following formula:

The Hypotrochoid equations can be written in terms of the ratio of R1 and R2, but with an additional parameter D. If D is equal to R2, then we have a Hypocycloid.

The shape uses the following version of the equation for a Hypotrochoid. The parameter M is used in place of D so that when D=0, we have a Hypocycloid.

In code, this corresponds to:

int gcd = Mathematics.NumberTheory.GCD(N, D);
double p = (double)N / gcd;
double q = (double)D / gcd;

Double k = (double)p / q;
K = k;
Function fx = new Function(t => R * ((k - 1) * Math.Cos(t + P) + 
                            Math.Pow(2, M) * Math.Cos((k - 1) * t + P)),
                          t => -R * ((k - 1) * Math.Sin(t + P) + (k - 1) * 
                            Math.Pow(2, M) * Math.Sin((k - 1) * t + P)));

Function fy = new Function(t => R * (k - 1) * Math.Sin(t + P) - R * 
                             Math.Pow(2, M) * Math.Sin((k - 1) * t + P),
                           t => R * (k - 1) * Math.Cos(t + P) - R * (k - 1) * 
                             Math.Pow(2, M) * Math.Cos((k - 1) * t + P));
CyclicCurve c = new CyclicCurve(fx, fy, 0, 2 * Math.PI * q);

We can see that:

t => R * ((k - 1) * Math.Cos(t + P) + Math.Pow(2, M) * Math.Cos((k - 1) * t + P))

corresponds to fx, while:

t => R * (k - 1) * Math.Sin(t + P) - R * Math.Pow(2, M) * Math.Sin((k - 1) * t + P)

corresponds to fy.

Again, this can be represented by a single complex function z(t), where fx is the real part of z and fy is the imaginary part of z(t).

If you substitute t with D*t, you get integer coefficients for the t variables.

The Hypotrochoid display grid allows you to see a group of Hypotrochoids together. Moving the M slider has a significant effect on their appearance.

Farris wheels

The Farris wheel brings us up to the present time. The curve was brought to aesthetic attention by Dr. Fark Farris in 1996, hence the name. We have seen that all of the curves so far can be written in the form:

where a and b are integers and A and B are complex. You could imagine extending z(t) to include a third term, and thus we have:

This can be expressed directly in terms of x(t) and t(t) as:

P refers to an overall phase shift. P, P1, P2, P3 have been scaled so that they go from -1 to 1 instead of -PI to PI. S is an overall scale factor, so W1, W2, W3 should be thought of as relative weights. In code, this corresponds to:

double maxRadius = Math.Abs(W1) + Math.Abs(W2) + Math.Abs(W3);
Double scaleFactor = R / maxRadius;
double pp1PI = (P + P1) * Math.PI;
double pp2PI = (P + P2) * Math.PI;
double pp3PI = (P + P3) * Math.PI;
Function fx = new Function(t => scaleFactor * (W1 * Math.Cos(F1 * t + pp1PI)
                                             + W2 * Math.Cos(F2 * t + pp2PI)
                                             + W3 * Math.Cos(F3 * t + pp3PI)),
                           t => scaleFactor * (-F1 * W1 * Math.Sin(F1 * t + pp1PI)
                                              - F2 * W2 * Math.Sin(F2 * t + pp2PI)
                                              - F3 * W3 * Math.Sin(F3 * t + pp3PI)));

Function fy = new Function(t => scaleFactor * (W1 * Math.Sin(F1 * t + pp1PI)
                                             + W2 * Math.Sin(F2 * t + pp2PI)
                                             + W3 * Math.Sin(F3 * t + pp3PI)),
                           t => scaleFactor * (F1 * W1 * Math.Cos(F1 * t + pp1PI)
                                             + F2 * W2 * Math.Cos(F2 * t + pp2PI)
                                             + F3 * W3 * Math.Cos(F3 * t + pp3PI)));
CyclicCurve c = new CyclicCurve(fx, fy, 0, 2 * Math.PI);

Some of the factors have been combined to reduce the number of arithmetic operations. This is a much richer shape than those that have preceded it, one capable of a great variety of forms. Note that for this one, the row and column can be changed as there are three frequencies, F1, F2, F3.

Lissajous curves

Lissajous curves were discovered/invented by Nathaniel Bowditch in 1815. They were later independently rediscovered by Antoine Lissajous in 1857 after whom they are named. (They are sometimes referred to as Bowditch curves.) The curves are most often seen on oscilloscopes.

The shape uses the following version of the formula where N and D are integers:

In code, this corresponds to:

int gcd = Mathematics.NumberTheory.GCD(Alpha,Beta);
double p = (double)Alpha / gcd;
double q = (double)Beta / gcd;

Function fx = new Function(t => A * Math.Sin(p * t + Delta ), 
                           t => A * p * Math.Cos(p * t + Delta  ));
Function fy = new Function(t => B * Math.Sin(q * t), 
                           t => B * q * Math.Cos(q * t  ));
CyclicCurve c = new CyclicCurve(fx, fy, 0, 2 * Math.PI  );

We can see that:

t => A * Math.Sin(p * t + Delta )

corresponds to fx, while:

t => B * Math.Sin(q * t)

corresponds to fy. The Lissajous demonstration shows the variety this shape is capable of. Note that changing Delta has an interesting effect on the shape.

Rose curves

Rose curves were discovered/invented by Luigi Guido Grandi around 1725. He thought they looked like flowers, hence the name. They can be represented in polar form as:

If alpha is rational, then the curve will close. For the shape, the following form is used with N and D as integers:

The shape is represented in code as:

int gcd = Mathematics.NumberTheory.GCD(N, D);
double p = (double)N / gcd;
double q = (double)D / gcd;
double omega = p / q;
Function fr = new Function(t => A * Math.Sin(omega * t), 
                           t =>  omega * A * Math.Cos(omega * t));
CyclicPolarCurve rc = new CyclicPolarCurve(fr, 0, 2 * q * Math.PI);

We can see that:

t => A * Math.Sin(omega * t)

corresponds to r, which corresponds to fy. The Rose demonstration shows the variety this shape is capable of.

Making your own

The included shapes represent a tiny fraction of the possible shapes that can be made from mathematically defined curves. It is extremely easy to create others. Throughout this section, I will use the Hypocycloid as an example as it is the simplest that shows all the features.

Basics

In order to create a shape, there are three things that must be done. First, you must inherit from FunctionalShapes.FunctionalClosedShapeBase. Second, you must override the Curve property. This is where you define the cure mathematically. In this case, the curve is being composed of two functions, fx and fy. The Function class takes two delegates in its constructor: one representing the values of the function, and one representing the derivative. Details on the definition of functions and curves can be found at GraphDisplay: a Bezier based control for graphing functions and curves.

protected override Mathematics.BaseClasses.CyclicCurveBase Curve
{
    get {
         //GCD for Greatest common Denominator
         int gcd = Mathematics.NumberTheory.GCD(N, D);
         double p = (double)N / gcd;
         double q = (double)D / gcd;
         Double m = (double)p / q - 1;
         Function fx = 
           new Function(t => R * m * Math.Cos(t + P) + R * Math.Cos(m * t + P),
                        t => -R * m * Math.Sin(t + P) - m * R * Math.Sin(m * t + P));

         Function fy = 
           new Function(t => R * m * Math.Sin(t + P) - R * Math.Sin(m * t + P),
                        t => R * m * Math.Cos(t + P) - R * m * Math.Cos(m * t + P));
         CyclicCurve c = new CyclicCurve(fx, fy,0,2* Math.PI * q);
         K = (double)p / q;
         return c;
    }  
}

The third requirement is that the segments property be defined. This defines the base number of Bezier segments to use before adjustment by SegmentAdjust.

protected override int Segments()
{
    return 200 + N*N + D*D;
}

Use with animation - Dependency Properties

Now it seems very likely that if you were going to go through the effort of defining a shape, it would not just be for a single shape but a parameterized family of shapes. This will work much better if dependency properties are used. That will enable you to use animation and the WPF dynamic render updating. Here is the property for the Hypocycloids phase shift.

public static DependencyProperty PProperty = 
   DependencyProperty.Register("P", typeof(double), ClassType, 
   new FrameworkPropertyMetadata(0.0, options, 
   new PropertyChangedCallback(OnPropChanged)));

[MathParameter("P", -Math.PI, Math.PI, false)]
public double P
{
    get
    {
        return (double)GetValue(PProperty);
    }
    set
    {
        SetValue(PProperty, value);
    }
}

Note that there is a PropertyChangedCallback. It nulls out the cached copy of the geometry. All dependency properties that affect the rendering need to do this. Setting FrameworkPropertyMetadataOptions is not sufficient.

private static void OnPropChanged(DependencyObject d, 
               DependencyPropertyChangedEventArgs e)
{
    Hypocycloid ps = (Hypocycloid)d;
    ps.mGeom = null;
}

Speaking of FrameworkPropertyMetadataOptions, I would advise the following options to be used. Also, once a variable like this is defined, it can be used in multiple dependency properties.

static FrameworkPropertyMetadataOptions options = 
            FrameworkPropertyMetadataOptions.AffectsRender 
          | FrameworkPropertyMetadataOptions.AffectsMeasure 
          | FrameworkPropertyMetadataOptions.AffectsArrange;

I would very strongly advise the use of the ClassType property when working with families of classes with dependency properties. It is simple, but it allows you to copy and paste between different classes.

static Type ClassType
{
    get { return typeof(Hypocycloid); }
}

Use with Shape Explorer - Custom attributes

One of the things that you might want to do to test out your new shape is use it in the Shape Explorer. While developing this article, one of the things I decided was that I was absolutely unwilling to make individual property editors for each shape. To solve this problem, a custom attribute was added to the class, the MathParameterAttribute.

public sealed class MathParameterAttribute:Attribute 
{
  /// <summary>
  /// The Friendly Name for the Parameter
  /// </summary>
  public string DisplayName { get; private set; }

  /// <summary>
  /// The Default Minimum value for the parameter(used by sliders)
  /// </summary>
  public Double MinValue { get; private set; }

  /// <summary>
  /// The default Maximum value for the parameter(used by sliders)
  /// </summary>
  public Double MaxValue { get; private set; }

  /// <summary>
  /// Is the parameter an integer(used by sliders)
  /// </summary>
  public bool IsInteger { get; private set; }

  /// <summary>
  /// Is the parameter Read only
  /// </summary>
  public bool IsReadOnly { get; set; }


  public MathParameterAttribute(String displayName, Double minValue, 
                                Double maxValue, bool isInteger)
  {
      DisplayName = displayName;
      MinValue = minValue;
      MaxValue = maxValue;
      IsInteger = isInteger;
      IsReadOnly = false;
  }

  public MathParameterAttribute(String displayName)
  {
      DisplayName = displayName;
      IsReadOnly = true;
  }
}

It deals with the two kinds of parameters that we have. In particular, the attribute supports sliders, giving minimum and maximum values to them as well as info on if they need to be restricted to integer values. Remember that you are free to put larger or smaller values into the textbox; the attributes only affect the sliders. A regular parameter would have an attribute like this:

[MathParameter("P", -Math.PI, Math.PI, false)]

This parameter has a display name of P. (The display name is there to support Greek letters and spaces in names.) The parameter runs form -PI to PI, and is not required to be an integer. The second type of parameter is the read only parameter. It only needs the display name. I am still working on my attribute UI framework, so I will give it the explanation it deserves in the future.

[MathParameter("K" )]

Use with GridDemonstration - Clone

You might also want to display grids of shapes. To use GridDemonstration, the Clone method must be defined and that is pretty much it.

public override FunctionalClosedShapeBase Clone()
{
    Hypocycloid clonedShape = new Hypocycloid()
    {
        N = this.N,
        D = this.D,
        R = this.R,
        P =this.P,
        Stretch = this.Stretch,
        Fill = this.Fill,
        Stroke = this.Stroke,
        StrokeThickness = this.StrokeThickness,
        SegmentAdjustment = this.SegmentAdjustment 
    };
    return clonedShape;
}

The shapes included also have drawing brushes for their formulae, but I am not sure I would advise adding them in a non-demo situation, as getting them to look nice is a bit of work.

Conclusion and Where to go from here

I hope that these shapes serve you all well. It was a bit of work, but also quite fun. These shapes, especially the Farris Wheel, are a delight to play around with. Even after all this, there is so much exciting stuff left to do. There are some further steps I am considering, and I would appreciate input.

  • Make a Silverlight version - I think that there is a reasonable chance that this can be converted to Silverlight.
  • Making Guilloché Patterns - The vast majority of the mathematical preliminaries are done so this is a possibility (Guilloché Patterns are the tracery like patterns seen on most currency).
  • Convert the shapes to geometry sources and make a few more decoration schemes. Fans of my Stacked Geometry Brush Factory - CodeProject might be interested in this.

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