After my previous spinning wait symbol, I decided to see how difficult it would be to create a Silverlight version of the Mac OSX wait cursor that I referenced in the previous post. The Mac OSX cursor is commonly referred to as the "Spinning Pizza of Death" or the "Marble of Doom" and in fact there is a Marble of Doom web site dedicated to the amount of time spent waiting while watching the spinning cursor. The Marble of Doom web site has a very nice and large version of the cursor using Flash although it doesn't have any vector information but is using video frames (they probably just published the final product and did not include the vector/animation information). The purpose of this post is to programmatically build the cursor and then in later posts to animate it.
Step 1: Decide on the initial interface properties
I realized quickly that I would need a little geometry to programmatically build the cursor, but the first step was to build the interface requirements. The essential properties were:
public int SliceCount { get; set; }
public double SliceCenterAngle { get; private set; }
public double SliceRotationAngle { get; set; }
public double RadiusX { get; set; }
public double RadiusY { get; set; }
The SliceCount determines how many slices or divisions to create, and the SliceCenterAngle is simply 360° / SliceCount. The SliceRotationAngle is the angle to twist or bend the slice. I decided to have a RadiusX and RadiusY to support ellipses in the future as well.
Step 2: Manually create a slice
Before I could programmatically create a slice, I needed to find out how to create a slice using XAML and Blend. The points on the slice would be in the center of the circle, and then two points on the circle determined by the SliceCenterAngle. The biggest question was how to create the arc and maintain the circular appearance. Fortunately, the Geometry Overview on MSDN was very helpful and got me started on the right track with the PathGeometry. I was able to create the simplest scenario with a single slice from a circle with four slices:
<Path Stroke="Black" StrokeThickness="1">
<Path.Data>
<PathGeometry>
<PathGeometry.Figures>
<PathFigure StartPoint="50,50">
<PathFigure.Segments>
<LineSegment Point="0,50" />
<ArcSegment Size="50,50" IsLargeArc="False"
RotationAngle="90" SweepDirection="CounterClockwise" Point="50,100" />
<LineSegment Point="50,50" />
</PathFigure.Segments>
</PathFigure>
</PathGeometry.Figures>
</PathGeometry>
</Path.Data>
</Path>
The next step was to create the same quarter-circle except with two slices:
<Path Stroke="Black" StrokeThickness="1">
<Path.Data>
<PathGeometry>
<PathGeometry.Figures>
<PathFigure StartPoint="50,50">
<PathFigure.Segments>
<LineSegment Point="0,50" />
<ArcSegment
Size="50,50"
Point="14.645,85.355"
RotationAngle="45"
IsLargeArc="False"
SweepDirection="CounterClockwise"
/>
<LineSegment Point="50,50" />
</PathFigure.Segments>
</PathFigure>
<PathFigure StartPoint="50,50">
<PathFigure.Segments>
<LineSegment Point="14.645,85.355" />
<ArcSegment
Size="50,50"
Point="50, 100"
RotationAngle="45"
IsLargeArc="False"
SweepDirection="CounterClockwise"
/>
<LineSegment Point="50,50" />
</PathFigure.Segments>
</PathFigure>
</PathGeometry.Figures>
</PathGeometry>
</Path.Data>
</Path>
Step 3: Create a slice programmatically
The general idea is to create one slice and then rotate the slice around the circle to create the complete circle.
/// <summary>
/// Create an ellipse using rotated slices to build the ellipse
/// </summary>
void CreateSlicePaths(Canvas cursorCanvas)
{
SliceCenterAngle = 360.0 / SliceCount;
// Create Slices
for (int index = 0; index < SliceCount; ++index)
{
PathFigure pathFigure = CreateSliceFigure();
PathGeometry pathGeometry = new PathGeometry();
pathGeometry.Figures.Add(pathFigure);
Path path = new Path();
path.Stroke = new SolidColorBrush(Color.FromArgb(255, 0, 0, 0));
path.StrokeThickness = 1.0;
path.Data = pathGeometry;
// Rotate the slice for all slices after the first slice
if (index > 0)
{
RotateTransform t1 = new RotateTransform();
t1.CenterX = RadiusX;
t1.CenterY = RadiusY;
t1.Angle = SliceCenterAngle * index;
TransformGroup transformGroup = new TransformGroup();
transformGroup.Children.Add(t1);
path.RenderTransform = transformGroup;
}
cursorCanvas.Children.Add(path);
}
}
/// <summary>
/// Create the base shape for the slice
/// </summary>
private PathFigure CreateSliceFigure()
{
// Start at the center of the ellipse
Point point0 = new Point(RadiusX, RadiusY);
// Next point is the left side of the ellipse
//Point point1 = new Point(0.0, RadiusY); // if no rotation
Point point1 = CalculatePointOnEllipse(SliceRotationAngle);
// Calculate the bottom point on the ellipse
Point point2 = CalculatePointOnEllipse(SliceRotationAngle + SliceCenterAngle);
// Starting point
PathFigure pathFigure = new PathFigure();
pathFigure.StartPoint = point0;
// Create the first line
LineSegment seg1 = new LineSegment();
seg1.Point = point1;
pathFigure.Segments.Add(seg1);
// Use an arc for the circular side
ArcSegment seg2 = new ArcSegment();
seg2.Point = point2;
seg2.Size = new Size(RadiusX, RadiusY);
seg2.RotationAngle = SliceCenterAngle;
seg2.IsLargeArc = (SliceCenterAngle > 180.0);
seg2.SweepDirection = SweepDirection.Counterclockwise;
pathFigure.Segments.Add(seg2);
// Close shape by going back to the starting point
LineSegment seg3 = new LineSegment();
seg3.Point = new Point(point0.X, point0.Y);
pathFigure.Segments.Add(seg3);
pathFigure.IsClosed = true;
pathFigure.IsFilled = true;
return pathFigure;
}
/// <summary>
/// Returns a point on the ellipse based on the rotationAngle, using
/// RadiusX/Y as the center (0, 0).
/// </summary>
private Point CalculatePointOnEllipse(double rotationAngle)
{
double angleRadians = rotationAngle * Math.PI / 180.0;
double x = Math.Cos(angleRadians);
x = RadiusX * x;
double y = Math.Sin(angleRadians);
y = RadiusY * y;
Point result = new Point(x, y);
result.X = RadiusX - x;
result.Y = RadiusY + y;
return result;
}
Step 4: Going Forward
Obviously this still needs a lot of improvement before it approaches the appeal of the Marble of Doom, which I will work on in the coming posts. However, the initial effort to create the "pizza" slices has been achieved and it is easier to build upon a base.
I added a grid in the background when I had some difficulty with the path geometry, but it is useful to I added two text boxes for the number of slices and rotation angle so that I could see the shape update dynamically.
Since I just used LineSegments to connect the slice points, the shape does not have the swirl or twist effect yet. Next time I will add the twist as well as rotation.
Technorati tags:
C#,
Silverlight
