Introduction
I recently came across the image pictured above on the Internet and wondered how to draw it. The image is sometimes called a Soddy Crescent. It is a deceptively simple image, composed of just a few circles. But if you think about an algorithm for drawing the circles, you may draw a blank. Fortunately, the problem was solved long ago. The image above is drawn using Soddy or kissing circles. This article explores how to use Soddy circles to draw the crescent. The drawing is done using WPF. A program to draw the crescent and animate it by varying the radii is developed. Along the way, methods to express circles as cubic Beziers and calculate the intersection points of 2 circles are provided. The article is about an algorithm, WPF is not its focus.
Soddy Circles
Given 3 circles that are pairwise tangent to one another, there exist 2 nonintersecting circles that are tangent to the 3 given circles. These circles are called the Inner and Outer Soddy circles. The 3 given circles plus any one of the Soddy circles produce 4 circles that are tangent to each other at 6 points. This is shown in the illustration below where the 3 given circles are black, the Inner Soddy circle is green and the Outer Soddy circle is blue.
The radius of four Soddy circles are related by the following formula:
The K terms are the curvature of the circles. The curvature is the reciprocal of a circle's radius. However, the radius is defined as negative for a circle if it encloses the other Soddy circles. In the diagram, the black circles have positive radiuses. The blue Outer Soddy circle's radius is taken to be negative as the other circles are enclosed by it.
Given 3 circles tangent to one another, the Soddy formula may be used to easily calculate the radius of a Soddy circle tangent to the given 3 circles. The formula provides only radius information. It's not useful until the center of the Soddy circle is also calculated. The center of an Inner Soddy circle may be calculated with simple geometric methods. There does not appear to be an easy way to calculate the center of an Outer Soddy circle.
Using the Soddy formula, one can solve for the radius of the forth circle in terms of 3 given circles by expanding the left hand side of the equation and using the Quadratic formula to solve for the forth radius. This yields the formula below:
Note the plus-minus symbol in the above equation. The forth radius has two solutions. Typically one for the Inner Soddy circle and one for the Outer Soddy circle. However, if one of the given circles encloses the other two, two Inner Soddy circles are returned.
Constructing the Soddy Crescent
The construction is begun by drawing a large circle with any convenient center. This will form the top of the crescent containing all subsequent circles. This means the large circle is an Outer Soddy circle. As the center is known a complicated calculation is avoided. The bottom of the crescent is formed by a smaller circle enclosed by the larger circle and tangent to it at the bottom. The larger and smaller circles share the same center x corordinate. The y cordinate of the smaller circle is the larger circle's y cordinate plus its radius minus the radius of the smaller circle. The illustration below shows the two circles forming the bottom and top of the crescent.
The Soddy formula requires 3 mutually tangent circles. The crescent provides 2. Another circle must be manually added before the formula can be used. This circle is added at the top, inside the crescent formed by the first 2 circles. Placing the third circle here allows its radius and center to be specified. The radius of the third circle is the difference between the radius of the large circle forming the top of the crescent and the radius of smaller circle forming the bottom of the crescent. The y cordinate of the third circle is the difference between the top of the large circle and the radius of the third circle. The top of the third circle is found by adding the y corordinate of its center to its radius. The diagram below shows the addition of the third circle to the crescent.
Now that there are 3 circles tangent to one another, the Soddy formula is used to find the radius of a forth circle tangent to the first 3 circles. Remember the curvature of the large circle is negative as it encloses the other circles. Typically the Soddy formula returns 2 radiuses, but for the forth circle both radiuses are equal.
The Soddy formula was used to determine the radius of the 4th circle. But where is it centered? The 4th circle is tangent to the first 3 circles and in particular, tangent to the 2 circles enclosed by the large Outer Soddy circle. The distance between the centers of two tangent circles that do not enclose each other is the sum of their radii. So if one takes the third circle, shown below in blue, and extend its radius by the radius of the forth circle, it forms a new circle which is the locus of all center points the forth circle may have. Similarly the same thing can be done for the second circle, show below in red. The two intersection points of these circles identify the possible centers of the fourth circle.
To solve for the centers of the Soddy circles inside the crescent, a method to calculate the intersection points of 2 circles is required. Initially, I thought I could liberate one by searching the internet. Surprisingly, I was not able to find one in C#. However, it's a well known problem whose solution is described all over the Web. In the file MyCircle.cs, the CircleIntersect
class is provided. CircleIntersect
's constructor take 2 circles as parameters. After the class is constructed, the intersection points for both circles is available in the intersects property which is a List of Points. The Count
for the list must be examined to determine if 0, 1, or 2 intersects were found. This corresponds to 2 circles that are disjoint, tangent to one another or overlapping. The circle intersections used for the Soddy crescent always return 2 intersections.
The fourth Soddy circle may now be drawn as its radius and center are known. The center has two possible locations, but for now only the left one will be used. The illustration below shows the crescent after the addition of the forth circle shown in blue.
The fifth and subsequent Soddy circles are drawn in a similar manner. But care is needed to chose the correct radius and center. The Soddy formula returns 2 radius values. When drawing Soddy circles from top to bottom, use the smaller radius. The circle intersection method also returns 2 possible values for the center. Only one is correct. When drawing circles in a counter clockwise fashion inside the crescent, use the second value. Conversely, the first value is used when proceeding in a clockwise direction.
Subsequent Soddy circles are added counterclockwise down the left side of the crescent just like the fifth circle. Each time the radius becomes smaller and is a little closer to the bottom of the crescent. One can add many circles, but they become vanishingly smaller, never reaching the bottom of the crescent. The right side of the crescent may be drawn by reflecting the circles on the left side across a vertical line passing through the centers of the first 3 circles. Alternately one can just start at the third circle again using a clockwise intersection point for the next center.
Drawing the Circles
This project uses cubic Bezier curves to draw circles which increases its complexity. So why use Bezier curves to draw circles? Well, it is what I had handy. You may find once you start using Bezier curves, you will try to do all shapes with them. It's easier to program if all your curves can be treated the same way especially when they are transformed, stretched, skewed or bent around another curve. Fortunately, drawing a circle with a cubic Bezier is a well known problem with a simple solution. One can employ the solution without knowing much about Bezier curves.
A Bezier curve has 4 points, a start point, an end point and 2 control points. The first control point determines the curve in the vicinity of the start point while the second control point determines the curve around the end point. The resulting curve passes through the start and end points, however typically it does not pass through the control points. When the control points appear in diagrams, they often are shown connected to their associated start or end point by a straight line.
A circle accurate enough for graphics work may be drawn with 4 connected Bezier curves. As four curves are used, each curve covers a single quadrant of the circle. The start and end points are placed on the circle 90 degrees apart at 0 and 90 degrees.The control points are placed on lines tangent to the circle at the start and end points. The distance from a start or end point to its associated control point is the same for both control points.
The illustration above shows shows the points defining the Bezier curve in the first quadrant of a circle. If the curve is being drawn counter clockwise, the points on the red dashed line are the starting point and first control point for the Bezier curve. Similarly the green dotted line are the end point and second control point of the Bezier. Both control points are the same distance away from their associated start or end point. The question is what is this distance? The formula for calculating the distance to a control point is shown below:
The Bezier curve used for the first quadrant is just like the other 3 curves for the remaining quadrants. The code for generating the 4 linked Bezier segments for a circle is in the class file MyCircle.cs. The method is named GenerateCurve
. MyCircle
is the class I use to define circles. It is instantiated with a radius and center and contains a definition for kappa. The Curve
class is used to define Bezier segments. It is composed of a start point and an array of Points
. Each Bezier segment has 3 entries in the Points
array. The first and second control points and the end point. If a Curve
contains multiple Bezier segments, the end point of the previous segment is taken to be the start point of the next segment.
public Curve GenerateCurve()
{
Curve curve = new Curve(4);
curve.startPoint = new Point(center.X, center.Y - radius);
curve.points[2] = new Point(center.X + radius, center.Y);
curve.points[5] = new Point(center.X, center.Y + radius);
curve.points[8] = new Point(center.X - radius, center.Y);
curve.points[11] = new Point(center.X, center.Y - radius);
double rk = radius * Kappa;
curve.points[0] = new Point(curve.startPoint.X + rk, curve.startPoint.Y);
curve.points[1] = new Point(curve.points[2].X, curve.points[2].Y - rk);
curve.points[3] = new Point(curve.points[2].X, curve.points[2].Y + rk);
curve.points[4] = new Point(curve.points[5].X + rk, curve.points[5].Y);
curve.points[6] = new Point(curve.points[5].X - rk, curve.points[5].Y);
curve.points[7] = new Point(curve.points[8].X, curve.points[8].Y + rk);
curve.points[9] = new Point(curve.points[8].X, curve.points[8].Y - rk);
curve.points[10] = new Point(curve.points[11].X-rk, curve.points[11].Y);
return curve;
}
About the Code
The MainWindow
is in file MainWindow.xaml.cs. It contains 2 variables that are central to the program. Their definitions are shown below:
private GrafCan gc;
private List<Curve> curves = new List<Curve>();
The gc
is responsible for mapping the logical points defined in a curve to their positions on a physical Canvas
. The gc
is instantiated once at the start of the program and whenever the Canvas
changes size. The gc knows the logical dimensions of the graphing axises and the physical dimensions of the Canvas
. The logical x axis always runs from 0 to 1. The logical y axis runs from -.5 to +.5 in this program but may be changed to whatever is convenient. The physical dimensions are whatever you resize the window to. If the Window dimensions are not square, the circles will appear to be ellipses. As an aide, the dimensions of the current canvas are displayed on the title bar.
The curves
is a list of the Curves
to be drawn on the canvas. Curves
can be added to the list or updated on the list. The Curves
in the list are displayed with the following code:
for (int i = 0; i < curves.Count; i++)
{
curves[i].AddCurveToCan(gc);
}
Recalling the algorithm to generate the Soddy crescent, everything was determined from the first 2 circles. If one assumes the first 2 circles centers have X coordinates at .5, the center of the logical X axis, only the radius of the of the circles is required to draw the Soddy Crescent. The method Zzzz
in MainWindow.xaml.cs returns a List of Curves
that constructs a Soddy Crescent given the radiuses of the first 2 circles. The returned List
is put in the curves
variable for display as follows.
curves = Zzzz(bigRadius, smallRadius);
The display is animated by repeatedly calling method Zzzz
with a consecutively larger smallRadius
. A DispatcherTimer
kicks off calls to method timer_Tick
to perform the animation. For simplicity, the timer is in WPF's thread rather than a background thread. Slow CPUs may get a jerky animation.
The algorithm discussion showed how given 3 Soddy circles and an intersection direction, a fourth Soddy Circle could be produced. This is done in a loop inside method Zzzz
. Looking inside Zzzz
one finds 2 loops like the one shown below that produce all the Soddy Circles. One loop produces Soddy Circles in the counter clockwise direction while the other procedes in a clockwise direction.
for (int i = 0; i < 50; i++)
{
lastCircle = NextCircle(circle, circle2, lastCircle, Direction.Left);
lastCircleCurve = lastCircle.GenerateCurve();
lastCircleCurve.style.fillBrush = new SolidColorBrush(Colors.DeepPink);
lastCircleCurve.style.brush = new SolidColorBrush(Colors.Blue);
newCurves.Add(lastCircleCurve);
}
The invocation of method NextCircle
in Zzzz
is where the messy work of calculating a Soddy Circle is done. NextCircle
instantiates a ForthSoddyCircle
class that contains a collection of the radiuses of possible Soddy circles. The smallest radius is selected and used to create two circles whose intersects are calculated to determine the center of the Soddy Circle. The intersects are calculating by using the circles to instantiate a CircleIntersect
class.
Using the Code
When the animation runs, change the window size to stretch the circles. The code was intended as a demonstration. It will take a little effort to import it into other programs. See the code in SoddyCircle.cs to calculate Soddy circle radiuses. Also the file MyCircle.cs contains the CircleIntersect
class which could be very handy in another project.
Points of Interest
The diagrams in this article were produced by WPF using only Bezier Curves. I hope I have sparked an interest in Beziers. When used programatically, they can be transformed in interesting ways. For example, the image below was produced by WPF using Beziers of cycloids that were bent around circles.
History
- August 2014: Initial release