Chapter 28. Geometries and Paths


The classes that derive from Shape include the (by now) familiar Rectangle, Ellipse, Line, Polyline, and Polygon. The only other class that derives from Shape is named Path, and it's absolutely the most powerful of them all. It encompasses the functionality of the other Shape classes and does much more besides. Path could potentially be the only vector-drawing class you'll ever need. The only real drawback of Path is that it tends to be a little verbose in comparison with the other Shape classes. However, toward the end of this chapter I'll show you a shortcut that Path implements that lets you be quite concise.

The only property that Path defines is Data, which you set to an object of type Geometry. The Geometry class itself is abstract, but seven classes derive from Geometry, as shown in this class hierarchy:

Object

      DispatcherObject (abstract)

               DependencyObject

                         Freezable (abstract)

                                   Animatable (abstract)

                                              Geometry (abstract)

                                                         LineGeometry

                                                         RectangleGeometry

                                                         EllipseGeometry

                                                         GeometryGroup

                                                         CombinedGeometry

                                                         PathGeometry

                                                         StreamGeometry

I've arranged those Geometry derivatives in the order in which I'll discuss them in this chapter. These classes represent the closest that WPF graphics come to encapsulating pure analytic geometry. You specify a Geometry object with points and lengths. The Geometry object does not draw itself. You must use another class (most often Path) to render the geometric object with the desired fill brush and pen properties. The markup looks something like this:

<Path Stroke="Blue" StrokeThickness="3" Fill="Red">     <Path.Data>         <EllipseGeometry ... />     </Path.Data> </Path> 


The LineGeometry class defines two properties: StartPoint and EndPoint. This stand-alone XAML file uses LineGeometry and Path to render two lines of different colors that cross each other.

LineGeometryDemo.xaml

[View full width]

<!-- === ================================================ LineGeometryDemo.xaml (c) 2006 by Charles Petzold ============================================= ====== --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Path Stroke="Blue" StrokeThickness="3"> <Path.Data> <LineGeometry StartPoint="96 96" EndPoint="192 192" /> </Path.Data> </Path> <Path Stroke="Red" StrokeThickness="3"> <Path.Data> <LineGeometry StartPoint="192 96" EndPoint="96 192" /> </Path.Data> </Path> </Canvas>



Just as you can have multiple Line elements on the Canvas, you can have multiple Path elements, and each Path element can potentially have its own Stroke color and StrokeThickness.

The RectangleGeometry class defines a Rect property (of type Rect, of course) that indicates the location and size of the rectangle, and RadiusX and RadiusY properties for the curvature of the corners. In XAML, you can set a property of type Rect by a string containing four numbers: the X coordinate of the upper-left corner, the Y coordinate of the upper-left corner, the width, and the height.

RectangleGeometryDemo.xaml

[View full width]

<!-- === ===================================================== RectangleGeometryDemo.xaml (c) 2006 by Charles Petzold ============================================= =========== --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Path Fill="Blue" Stroke="Red" StrokeThickness="3"> <Path.Data> <RectangleGeometry Rect="96 48 288 192" RadiusX="24" RadiusY="24" /> </Path.Data> </Path> </Canvas>



The StartPoint and EndPoint properties defined by LineGeometryand the Rect, RadiusX, and RadiusY properties defined by RectangleGeometryare all backed by dependency properties, which means that these properties can be animated (as you'll see in Chapter 30) and they can be targets of data bindings.

EllipseGeometry contains a constructor that lets you base the ellipse on a Rect object. However, this facility is not available when you define an EllipseGeometry in XAML. Instead, you specify the center of the ellipse with a Center property of type Point and the dimensions of the ellipse with RadiusX and RadiusY properties of type double. All three properties are backed by dependency properties.

EllipseGeometryDemo.xaml

[View full width]

<!-- === =================================================== EllipseGeometryDemo.xaml (c) 2006 by Charles Petzold ============================================= ========= --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Path Fill="Blue" Stroke="Red" StrokeThickness="3"> <Path.Data> <EllipseGeometry Center="196, 144" RadiusX="144" RadiusY="96" /> </Path.Data> </Path> </Canvas>



You might recall the DrawCircles program from Chapter 9 that lets you use the mouse to draw circles on a Canvas. The initial mouse click indicates the center of the circle and the later position of the mouse governs the radius. But that program uses an Ellipse element, which requires that the size of the ellipse be specified by its width and height. The location of the Ellipse on the Canvas is given by the Left and Top attached properties. When the DrawCircles program responds to mouse movements by expanding or contracting the circle, it needs to keep the ellipse centered on the same point, which requires moving the ellipse as well as increasing its size. If that program used EllipseGeometry instead, the logic would be simplified.

The Path.Data property element can have only one child of type Geometry. As you've seen, you can get around this limitation by using multiple Path elements. Or, you can mix Path elements with other elements such as Rectangle.

Another way to get around the limitation is to make a GeometryGroup object the child of Path.Data. GeometryGroup inherits from Geometry but has a property named Children of type GeometryCollection, which is a collection of Geometry objects. GeometryGroup can host multiple Geometry children. The markup might look something like this:

<Path Fill="Gold" Stroke="Pink" ...>     <Path.Data>         <GeometryGroup>             <EllipseGeometry ... />             <LineGeometry ... />             <RectangleGeometry ... />         </GeometryGroup>     </Path.Data> </Path> 


Because all the Geometry objects in the GeometryGroup are part of the same Path element, they all have the same Stroke and Fill brushes. That's one major difference between using GeometryGroup rather than multiple Path elements.

The other major difference is illustrated in this XAML file. On the left are two overlapping rectangles rendered by two separate Path elements. On the right are two overlapping rectangles that are part of the same GeometryGroup.

OverlappingRectangles.xaml

[View full width]

<!-- === ===================================================== OverlappingRectangles.xaml (c) 2006 by Charles Petzold ============================================= =========== --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Path Fill="Gold" Stroke="Red" StrokeThickness="3"> <Path.Data> <RectangleGeometry Rect="96 96 192 192" /> </Path.Data> </Path> <Path Fill="Gold" Stroke="Red" StrokeThickness="3"> <Path.Data> <RectangleGeometry Rect="192 192 192 192" /> </Path.Data> </Path> <Path Fill="Gold" Stroke="Red" StrokeThickness="3"> <Path.Data> <GeometryGroup> <RectangleGeometry Rect="480 96 192 192" /> <RectangleGeometry Rect="576 192 192 192" /> </GeometryGroup> </Path.Data> </Path> </Canvas>



With the first pair of rectangles, one rectangle simply sits on top of the other. But for the second pair of rectangles joined in a GeometryGroup, the area where they overlap is transparent. What's happening here is exactly what happens when a Polygon element has overlapping boundaries. GeometryGroup defines its own FillRule property, and the default value of EvenOdd doesn't fill enclosed areas separated from infinity by an even number of boundary lines. You can alternatively set the FillRule to NonZero, like this:

<GeometryGroup FillRule="NonZero"> 


In that case, the overlapping area will be filled, but the second pair of rectangles in the program still won't look like the first pair. With the first pair, the second rectangle obscures part of the first rectangle's border. With the second pair, all the borders are visible.

You can get some interesting effects by combining multiple geometries in the same GeometryGroup.

FourOverlappingCircles.cs

[View full width]

<!-- === ====================================================== FourOverlappingCircles.xaml (c) 2006 by Charles Petzold ============================================= ============ --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Path Fill="Blue" Stroke="Red" StrokeThickness="3"> <Path.Data> <GeometryGroup> <EllipseGeometry Center="150 150" RadiusX="100" RadiusY="100" /> <EllipseGeometry Center="250 150" RadiusX="100" RadiusY="100" /> <EllipseGeometry Center="150 250" RadiusX="100" RadiusY="100" /> <EllipseGeometry Center="250 250" RadiusX="100" RadiusY="100" /> </GeometryGroup> </Path.Data> </Path> </Canvas>



That's the GeometryGroup class, and it's easy to confuse that class with another Geometry derivative named CombinedGeometry. But CombinedGeometry is quite different. First, it doesn't have a Children property. Instead, it has two properties named Geometry1 and Geometry2. CombinedGeometry is a combination of two and only two other geometries.

The second difference between GeometryGroup and CombinedGeometry is that CombinedGeometry doesn't have a FillRule property. Instead it has a GeometryCombineMode property that you set to a member of the GeometryCombineMode enumeration: Union, Intersect, Xor, or Exclude. The first three options work like visual Venn diagrams; the Exclude option creates a geometry that consists of everything in Geometry2 that's not also in Geometry1.

The following stand-alone XAML program demonstrates the four GeometryCombineMode options with two overlapping circles.

CombinedGeometryModes.xaml

[View full width]

<!-- === ===================================================== CombinedGeometryModes.xaml (c) 2006 by Charles Petzold ============================================= =========== --> <UniformGrid xmlns="http://schemas.microsoft.com/ winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" Rows="2" Columns="2" TextBlock .FontSize="12pt"> <UniformGrid.Resources> <Style TargetType="{x:Type Path}"> <Setter Property="HorizontalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="Fill" Value="Blue" /> <Setter Property="Stroke" Value="Red" /> <Setter Property="StrokeThickness" Value="5" /> </Style> </UniformGrid.Resources> <!-- GeometryCombineMode = "Union". --> <Grid> <TextBlock HorizontalAlignment="Center"> GeometryCombineMode="Union" </TextBlock> <Path> <Path.Data> <CombinedGeometry GeometryCombineMode="Union"> <CombinedGeometry.Geometry1> <EllipseGeometry Center="96 96" RadiusX="96" RadiusY="96" /> </CombinedGeometry.Geometry1> <CombinedGeometry.Geometry2> <EllipseGeometry Center="48 48" RadiusX="96" RadiusY="96" /> </CombinedGeometry.Geometry2> </CombinedGeometry> </Path.Data> </Path> </Grid> <!-- GeometryCombineMode = "Intersect". --> <Grid> <TextBlock HorizontalAlignment="Center"> GeometryCombineMode="Intersect" </TextBlock> <Path> <Path.Data> <CombinedGeometry GeometryCombineMode="Intersect"> <CombinedGeometry.Geometry1> <EllipseGeometry Center="96 96" RadiusX="96" RadiusY="96" /> </CombinedGeometry.Geometry1> <CombinedGeometry.Geometry2> <EllipseGeometry Center="48 48" RadiusX="96" RadiusY="96" /> </CombinedGeometry.Geometry2> </CombinedGeometry> </Path.Data> </Path> </Grid> <!-- GeometryCombineMode = "Xor". --> <Grid> <TextBlock HorizontalAlignment="Center"> GeometryCombineMode="Xor" </TextBlock> <Path> <Path.Data> <CombinedGeometry GeometryCombineMode="Xor"> <CombinedGeometry.Geometry1> <EllipseGeometry Center="96 96" RadiusX="96" RadiusY="96" /> </CombinedGeometry.Geometry1> <CombinedGeometry.Geometry2> <EllipseGeometry Center="48 48" RadiusX="96" RadiusY="96" /> </CombinedGeometry.Geometry2> </CombinedGeometry> </Path.Data> </Path> </Grid> <!-- GeometryCombineMode = "Exclude". --> <Grid> <TextBlock HorizontalAlignment="Center"> GeometryCombineMode="Exclude" </TextBlock> <Path> <Path.Data> <CombinedGeometry GeometryCombineMode="Exclude"> <CombinedGeometry.Geometry1> <EllipseGeometry Center="96 96" RadiusX="96" RadiusY="96" /> </CombinedGeometry.Geometry1> <CombinedGeometry.Geometry2> <EllipseGeometry Center="48 48" RadiusX="96" RadiusY="96" /> </CombinedGeometry.Geometry2> </CombinedGeometry> </Path.Data> </Path> </Grid> </UniformGrid>



Notice how different these images are from every other type of combination of objects you've seen so far. Even the Union optionwhich at first seems to offer no benefits over two simple overlapping figuresproduces something different. The border of the combined circles goes around the composite object rather than the original objects. The CombinedGeometry can give you some effects that might otherwise be difficult. One example is this image of a dumbbell.

Dumbbell.xaml

[View full width]

<!-- =========================================== Dumbbell.xaml (c) 2006 by Charles Petzold =========================================== --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Path Fill="DarkGray" Stroke="Black" StrokeThickness="5"> <Path.Data> <CombinedGeometry GeometryCombineMode="Union"> <CombinedGeometry.Geometry1> <CombinedGeometry GeometryCombineMode="Union"> <CombinedGeometry.Geometry1> <EllipseGeometry Center="100 100" RadiusX="50" RadiusY="50" /> </CombinedGeometry.Geometry1> <CombinedGeometry.Geometry2> <RectangleGeometry Rect="100 75 200 50" /> </CombinedGeometry.Geometry2> </CombinedGeometry> </CombinedGeometry.Geometry1> <CombinedGeometry.Geometry2> <EllipseGeometry Center="300 100" RadiusX="50" RadiusY="50" /> </CombinedGeometry.Geometry2> </CombinedGeometry> </Path.Data> </Path> </Canvas>



This image makes use of two nested CombinedGeometry objects. The first combines an ellipse and a rectangle; the second combines that CombinedGeometry with another ellipse. All three objects are combined with the Union mode.

It is with the Geometry derivative named PathGeometry that we really get into the central focus of this whole geometry system. A graphics path is a collection of straight lines and curves, some of which might or might not be connected to each other. Any set of connected lines and curves within the path is known as a subpath, orto use the synonymous term consistent with the WPF classesa figure. Thus, a path is composed of zero or more figures.

Each figure can be either open or closed. A figure is closed if the end of the last line in the figure is connected to the beginning of the first line. Otherwise, the figure is open. (Traditionally, an open path cannot be filled with a brush; in the WPF, filling is independent of closure.)

Here's the brief rundown: A PathGeometry object is a collection of one or more PathFigure objects. Each PathFigure is a collection of connected PathSegment objects. A PathSegment is a single straight line or curve.

Now for the details. The PathGeometry class defines a FillRule property that governs how figures in the path are filled, and a Figures property of type PathFigureCollection, which is the collection of PathFigure objects.

PathFigure derives from Animatable and is sealed. (That is, no other classes derive from PathFigure.) PathFigure defines two Boolean properties named IsClosed and IsFilled. By default, IsClosed is false. If you set the property to true, the figure will be automatically closed. A straight line will be automatically added from the last point of the figure to the start point if necessary to close the figure. The default value of IsFilled is true and governs whether internal areas are colored with a brush. An area is filled as if the area is closed even if the area is not closed. (You encountered a similar concept in Chapter 27 in the way that Polyline fills areas.)

Remember that a figure is a series of connected straight lines and curves. The figure has to start at some particular point, and that point is the StartPoint property of PathFigure. PathFigure also defines a property named Segments of type PathSegmentCollection, which is a collection of PathSegment objects. PathSegment is an abstract class with seven derivatives. The following class hierarchy shows all the path-related classes:

Object

       DispatcherObject (abstract)

                DependencyObject

                           Freezable (abstract)

                                     Animatable (abstract)

                                               PathFigure

                                               PathFigureCollection

                                               PathSegment (abstract)

                                                          ArcSegment

                                                          BezierSegment

                                                          LineSegment

                                                          PolyBezierSegment

                                                          PolyLineSegment

                                                          PolyQuadraticBezierSegment

                                                          QuadraticBezierSegment

                                               PathSegmentCollection

In summary, the PathGeometry class has a Figures property of type PathFigureCollection. The PathFigure class has a Segments property of type PathSegmentCollection.

For readers who have been wondering where Bézier curves have been hidden in the WPF graphics system, here they are. They come in two varieties: the normal cubic form and a faster quadratic variation. Here also are arcs, which are curves on the circumference of an ellipse. These are the only classes in WPF that explicitly support Bézier curves and arcs. Even if you're drawing graphics at the DrawingContext level (either in an OnRender method or by creating a DrawingVisual object), you don't have explicit Bézier or arc-drawing methods. Instead, you must draw Bézier curves or arcs using the DrawGeometry method or (more remotely) the DrawDrawing method.

The classes that derive from PathSegment also provide you with the only way to create Geometry objects that describe arbitrary polygons. You might be able to patch polygons together using CombinedGeometry and rotation, but you'd probably go insane in the process.

In any figure, the PathFigure object indicates the starting point of the figure in the StartPoint property. The LineSegment class therefore has only a single Point property that you set to the end point of a line. Here's a trivial path that consists of a single straight line.

TrivialPath.xaml

[View full width]

<!-- ============================================== TrivialPath.xaml (c) 2006 by Charles Petzold ============================================= = --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Path Stroke="Blue" StrokeThickness="3"> <Path.Data> <PathGeometry> <PathFigure StartPoint="96 96"> <LineSegment Point="384 192" /> </PathFigure> </PathGeometry> </Path.Data> </Path> </Canvas>



To draw multiple connected straight lines, you can use multiple LineSegment objects. Each LineSegment object continues the figure with another segment that starts at the end of the previous segment. Here's a small XAML file that draws a five-pointed star with four LineSegment objects.

MultipleLineSegments.xaml

[View full width]

<!-- === ==================================================== MultipleLineSegments.xaml (c) 2006 by Charles Petzold ============================================= ========== --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Path Fill="Aqua" Stroke="Maroon" StrokeThickness="3"> <Path.Data> <PathGeometry> <PathFigure StartPoint="144 72"> <LineSegment Point="200 246" /> <LineSegment Point="53 138" /> <LineSegment Point="235 138" /> <LineSegment Point="88 246" /> </PathFigure> </PathGeometry> </Path.Data> </Path> </Canvas>



The star drawn by this markup might appear a little strange. The five points of the star are filled and the center is not filledwhich we might expect from the default FillRule of EvenOddbut the line from the lower-left point to the top point is missing. And sure enough, if you look at the markup, you'll see that the four LineSegment objects draw only four lines. You can fix this problem and make the star appear more normal by including a fifth LineSegment object with the same point specified in PathFigure:

<LineSegment Point="144 72" /> 


Or, you can set the IsClosed property of the PathFigure to true:

<PathFigure StartPoint="144 72" IsClosed="True"> 


Setting IsClosed to true automatically creates an additional straight line back to the starting point. The existence of this last line does not govern whether an internal area is filled. Whether areas are filled is based on the IsFilled property of PathFigure (which is true by default) and the FillRule of the Path element (which is EvenOdd by default).

Another approach to drawing a series of connected straight lines is the PolyLineSegment. The Points property defined by PolyLineSegment is an object of type PointCollection, and as with the Points property defined by the Polyline element, you can set it to a string of multiple points in XAML. In this file I've explicitly set the IsClosed property of PathFigure to true:

PolyLineSegmentDemo.xaml

[View full width]

<!-- === =================================================== PolyLineSegmentDemo.xaml (c) 2006 by Charles Petzold ============================================= ========= --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Path Fill="Aqua" Stroke="Maroon" StrokeThickness="3"> <Path.Data> <PathGeometry> <PathFigure StartPoint="144 72" IsClosed="True"> <PolyLineSegment Points="200 246, 53 138, 235 138, 88 246" /> </PathFigure> </PathGeometry> </Path.Data> </Path> </Canvas>



If this PathFigure contained an additional segment following the PolyLineSegment, it would continue from the last point of the PolyLineSegment. The PathFigure element always defines the start of the figure. Each segment contains one or more points that continue the figure from the last preceding point.

Five classes define a FillRule property. Both the Polyline and Polygon elements in the Shapes library define one, as well as GeometryGroup, PathGeometry, and StreamGeometry (which is conceptually similar to PathGeometry). With PathGeometry, the FillRule property also governs how overlapping multiple figures in the geometry are filled. Here's an example that combines two overlapping stars in the same PathGeometry.

OverlappingStars.xaml

[View full width]

<!-- === ================================================ OverlappingStars.xaml (c) 2006 by Charles Petzold ============================================= ====== --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Path Fill="Aqua" Stroke="Maroon" StrokeThickness="3"> <Path.Data> <PathGeometry> <PathFigure StartPoint="144 72" IsClosed="True"> <PolyLineSegment Points="200 246, 53 138, 235 138, 88 246" /> </PathFigure> <PathFigure StartPoint="168 96" IsClosed="True"> <PolyLineSegment Points="224 260, 77 162, 259 162, 112 270" /> </PathFigure> </PathGeometry> </Path.Data> </Path> </Canvas>



The second star is offset from the first by one-quarter inch horizontally and one-quarter inch vertically. You'll see that the two stars are taken together to determine whether internal areas are filled based on the default FillRule of EvenOdd. If you enclose that PathGeometry in a GeometryGroup, the FillRule setting on the GeometryGroup takes precedence and the FillRule setting on the PathGeometry is ignored. Both stars will still be analyzed together to determine filling. If you need overlapping paths to be filled independently of each other, put them in separate Path elements.

The ArcSegment class defines a curved line on the circumference of an ellipsea concept that turns out to be a little more complex than it first appears. Like LineSegment, the ArcSegment element defines just one point, and an arc is drawn from the last preceding point to the point specified in the ArcSegment element. However, some additional information is required in the ArcSegment element. The curve that connects the two points is an arc on the circumference of an ellipse, so the two radii of that ellipse need to be supplied. In the simplest case, the two radii are equal and the curve that connects the two points is actually an arc on the circumference of a circle. Still, in general there are four ways that an arc of that circle can connect the two points. Which one you get is governed by the SweepDirection property of ArcSegment, which you set to either Clockwise or Counterclockwise (the default) and the Boolean IsLargeArc property (false by default). The four possible arcs are demonstrated in the following program with different colors.

ArcPossibilities.xaml

[View full width]

<!-- === ================================================ ArcPossibilities.xaml (c) 2006 by Charles Petzold ============================================= ====== --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <!-- Counterclockwise (default), small arc (default) --> <Path Stroke="Red" StrokeThickness="3"> <Path.Data> <PathGeometry> <PathFigure StartPoint="144 144"> <ArcSegment Point="240 240" Size="96 96" /> </PathFigure> </PathGeometry> </Path.Data> </Path> <!-- Counterclockwise (default), IsLargeArc --> <Path Stroke="Blue" StrokeThickness="3"> <Path.Data> <PathGeometry> <PathFigure StartPoint="144 144"> <ArcSegment Point="240 240" Size="96 96" IsLargeArc="True" /> </PathFigure> </PathGeometry> </Path.Data> </Path> <!-- Clockwise, small arc (default) --> <Path Stroke="Green" StrokeThickness="3"> <Path.Data> <PathGeometry> <PathFigure StartPoint="144 144"> <ArcSegment Point="240 240" Size="96 96" SweepDirection="ClockWise" /> </PathFigure> </PathGeometry> </Path.Data> </Path> <!-- Clockwise, IsLargeArc --> <Path Stroke="Purple" StrokeThickness="3"> <Path.Data> <PathGeometry> <PathFigure StartPoint="144 144"> <ArcSegment Point="240 240" Size="96 96" SweepDirection="ClockWise" IsLargeArc="True" /> </PathFigure> </PathGeometry> </Path.Data> </Path> </Canvas>



Each of the four Path elements uses the same starting point of (144, 144) and ArcSegment point of (240, 240). In all four cases the points are connected with an arc on the circumference of a circle with a radius of 96. But the four combinations of SweepDirection and IsLargeArc draw four different arcs in four different colors.

And if this isn't enough flexibility for you, ArcSegment defines another property named RotationAngle that indicates the clockwise rotation of the ellipse whose circumference connects the points. The following program is identical to ArcPossibilities.xaml except that it uses an ellipse with a horizontal radius of 144 and a vertical radius of 96, rotated clockwise by 45 degrees.

ArcPossibilities2.xaml

[View full width]

<!-- === ================================================= ArcPossibilities2.xaml (c) 2006 by Charles Petzold ============================================= ======= --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <!-- Counterclockwise (default), small arc (default) --> <Path Stroke="Red" StrokeThickness="3"> <Path.Data> <PathGeometry> <PathFigure StartPoint="144 144"> <ArcSegment Point="240 240" Size="144 96" RotationAngle="45" /> </PathFigure> </PathGeometry> </Path.Data> </Path> <!-- Counterclockwise (default), IsLargeArc --> <Path Stroke="Blue" StrokeThickness="3"> <Path.Data> <PathGeometry> <PathFigure StartPoint="144 144"> <ArcSegment Point="240 240" Size="144 96" RotationAngle="45" IsLargeArc="True" /> </PathFigure> </PathGeometry> </Path.Data> </Path> <!-- Clockwise, small arc (default) --> <Path Stroke="Green" StrokeThickness="3"> <Path.Data> <PathGeometry> <PathFigure StartPoint="144 144"> <ArcSegment Point="240 240" Size="144 96" RotationAngle="45" SweepDirection="ClockWise" /> </PathFigure> </PathGeometry> </Path.Data> </Path> <!-- Clockwise, IsLargeArc --> <Path Stroke="Purple" StrokeThickness="3"> <Path.Data> <PathGeometry> <PathFigure StartPoint="144 144"> <ArcSegment Point="240 240" Size="144 96" RotationAngle="45" SweepDirection="ClockWise" IsLargeArc="True" /> </PathFigure> </PathGeometry> </Path.Data> </Path> </Canvas>



The following program is closer to one that you'd find in real life. It draws something that might be a piece of an imaginary machinean outline consisting of straight lines and arcs, and also a hole comprising two arc segments.

FigureWithArcs.xaml

[View full width]

<!-- ================================================= FigureWithArcs.xaml (c) 2006 by Charles Petzold ============================================= ==== --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Path Fill="Aqua" Stroke="Maroon" StrokeThickness="3"> <Path.Data> <PathGeometry> <PathFigure StartPoint="192 192"> <ArcSegment Point="192 288" Size="48 48" /> <LineSegment Point="480 288" /> <ArcSegment Point="480 192" Size="48 48" /> <LineSegment Point="384 192" /> <ArcSegment Point="288 192" Size="48 48" /> <LineSegment Point="192 192" /> </PathFigure> <PathFigure StartPoint="336 200" IsClosed="True"> <ArcSegment Point="336 176" Size="12 12" /> <ArcSegment Point="336 200" Size="12 12" /> </PathFigure> </PathGeometry> </Path.Data> </Path> </Canvas>



The second PathFigure effectively draws a circle by connecting two semicircle arcs.

Most of the terminology used in computer graphics comes from geometry. But in one case, an actual person's name has been enshrined in the names of graphics drawing functions.

Pierre Etienne Bézier was born in Paris in 1910 into a family of engineers. He received a degree in mechanical engineering in 1930 and a second degree in electrical engineering the following year. In 1933 he began working at the French automotive company Renault, where he remained until 1975. During the 1950s, Bézier was responsible for implementing some of the first drilling and milling machines that operated under NCthat is, numerical control (a term rarely used these days).

Beginning in 1960, much of Bézier's work was centered around the UNISURF program, an early CAD/CAM system used at Renault for interactively designing automobile parts. Such a system required mathematical definitions of complex curves that designers could manipulate without knowing about the underlying mathematics. These curves could then be used in manufacturing processes. From this work came the curve that now bears Bézier's name. Pierre Bézier died in 1999.

The Bézier curve is a spline, which is a curve used to approximate discrete data with a smooth continuous function. A single cubic Bézier curve is uniquely defined by four points, which can be labeled p0, p1, p2, and p3. The curve begins at p0 and ends at p3. Often p0 is referred to as the start point (or begin point) of the curve and p3 is referred to as the end point, but sometimes both points are collectively called end points. The points p1 and p2 are called control points. These two control points seem to function like magnets in pulling the curve toward them. Here's a simple Bézier curve showing the two end points and two control points.

Notice that the curve begins at p0 by heading toward p1, but then abandons that trip and instead heads toward p2. Without touching p2 either, the curve ends at p3. Here's another Bézier curve:

Only rarely does the Bézier curve pass through the two control points. However, if you position both control points between the end points, the Bézier curve becomes a straight line and passes through them:

At the other extreme, it's even possible to choose points that make the Bézier curve do a little loop:

To draw a single Bézier curve you need four points. In the context of a PathFigure, the first point (p0 in the diagrams) is provided by the StartPoint property of the PathFigure object or the last point of the preceding segment. The two control points and the end point are provided by the Point1, Point2, and Point3 properties of the BezierSegment class. The numbering of these three properties agrees with the numbering I've used in the preceding diagrams.

Here's a small XAML program that produces a figure similar to the first Bézier diagram on the previous page.

SingleBezier.xaml

[View full width]

<!-- =============================================== SingleBezier.xaml (c) 2006 by Charles Petzold ============================================= == --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Path Fill="Red" Stroke="Blue" StrokeThickness="3"> <Path.Data> <GeometryGroup> <PathGeometry> <PathFigure x:Name="fig" StartPoint="50 150" IsFilled="False" > <BezierSegment Point1="25 25" Point2="400 300" Point3="450 150" /> </PathFigure> </PathGeometry> <EllipseGeometry Center="{Binding ElementName=fig, Path=StartPoint}" RadiusX="5" RadiusY="5" /> <EllipseGeometry Center="{Binding ElementName=fig, Path=Segments[0].Point1}" RadiusX="5" RadiusY="5" /> <EllipseGeometry Center="{Binding ElementName=fig, Path=Segments[0].Point2}" RadiusX="5" RadiusY="5" /> <EllipseGeometry Center="{Binding ElementName=fig, Path=Segments[0].Point3}" RadiusX="5" RadiusY="5" /> </GeometryGroup> </Path.Data> </Path> </Canvas>



To the actual Bézier curve I've also added four EllipseGeometry objects with bindings to display the end points and control points.

The Bézier spline has achieved much prominence in graphics programming for several reasons, all of which become evident when you experiment with the curves a bit. That's the purpose of the following BezierExperimenter project, which consists of a XAML file and a C# file.

The XAML file lays out a Canvas beginning with four EllipseGeometry objects. These display the four points that define the Bézier curve and they are given x:Name attributes that become variable names that the C# file uses to actually set the points.

BezierExperimenter.xaml

[View full width]

<!-- === ================================================== BezierExperimenter.xaml (c) 2006 by Charles Petzold ============================================= ======== --> <Window xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.BezierExperimenter" Title="Bezier Experimenter"> <Canvas Name="canvas"> <!-- Draw the four points defining the curve. --> <Path Fill="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}"> <Path.Data> <GeometryGroup> <EllipseGeometry x :Name="ptStart" RadiusX="2" RadiusY="2" /> <EllipseGeometry x :Name="ptCtrl1" RadiusX="2" RadiusY="2" /> <EllipseGeometry x :Name="ptCtrl2" RadiusX="2" RadiusY="2" /> <EllipseGeometry x :Name="ptEnd" RadiusX="2" RadiusY="2" /> </GeometryGroup> </Path.Data> </Path> <!-- Draw the curve itself. --> <Path Stroke="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}"> <Path.Data> <PathGeometry> <PathGeometry.Figures> <PathFigure StartPoint="{Binding ElementName=ptStart, Path=Center}"> <BezierSegment Point1="{Binding ElementName=ptCtrl1, Path=Center}" Point2="{Binding ElementName=ptCtrl2, Path=Center}" Point3="{Binding ElementName=ptEnd, Path=Center}" /> </PathFigure> </PathGeometry.Figures> </PathGeometry> </Path.Data> </Path> <!-- Draw gray lines connecting end points and control points. --> <Path Stroke="{DynamicResource {x:Static SystemColors .GrayTextBrushKey}}"> <Path.Data> <GeometryGroup> <LineGeometry StartPoint="{Binding ElementName=ptStart, Path=Center}" EndPoint="{Binding ElementName=ptCtrl1, Path=Center}" /> <LineGeometry StartPoint="{Binding ElementName=ptEnd, Path=Center}" EndPoint="{Binding ElementName=ptCtrl2, Path=Center}" /> </GeometryGroup> </Path.Data> </Path> <!-- Display some labels with the actual points. --> <Label Canvas.Left="{Binding ElementName=ptStart, Path=Center.X}" Canvas.Top="{Binding ElementName=ptStart, Path=Center.Y}" Content="{Binding ElementName=ptStart, Path=Center}" /> <Label Canvas.Left="{Binding ElementName=ptCtrl1, Path=Center.X}" Canvas.Top="{Binding ElementName=ptCtrl1, Path=Center.Y}" Content="{Binding ElementName=ptCtrl1, Path=Center}" /> <Label Canvas.Left="{Binding ElementName=ptCtrl2, Path=Center.X}" Canvas.Top="{Binding ElementName=ptCtrl2, Path=Center.Y}" Content="{Binding ElementName=ptCtrl2, Path=Center}" /> <Label Canvas.Left="{Binding ElementName=ptEnd, Path=Center.X}" Canvas.Top="{Binding ElementName=ptEnd, Path=Center.Y}" Content="{Binding ElementName=ptEnd , Path=Center}" /> </Canvas> </Window>



The Canvas continues with a bunch of bindings, all of which reference the Center properties of the first four EllipseGeometry objects. A PathSegment draws the actual Bézier curve, and two gray lines connect the two end points with the two control points. (You'll see the rationale behind this shortly.) Finally, some labels print the actual values of the points, just in case you find a nice Bézier curve you want to reuse in some XAML markup.

The C# portion of the program resets the four points whenever the size of the window changes.

BezierExperimenter.cs

[View full width]

//--------------------------------------------------- // BezierExperimenter.cs (c) 2006 by Charles Petzold //--------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.BezierExperimenter { public partial class BezierExperimenter : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new BezierExperimenter()); } public BezierExperimenter() { InitializeComponent(); canvas.SizeChanged += CanvasOnSizeChanged; } // When the Canvas size changes, reset the four points. protected virtual void CanvasOnSizeChanged (object sender, SizeChangedEventArgs args) { ptStart.Center = new Point(args .NewSize.Width / 4, args .NewSize.Height / 2); ptCtrl1.Center = new Point(args .NewSize.Width / 2, args .NewSize.Height / 4); ptCtrl2.Center = new Point(args .NewSize.Width / 2, 3 * args .NewSize.Height / 4); ptEnd.Center = new Point(3 * args .NewSize.Width / 4, args.NewSize .Height / 2); } // Change the control points based on mouse clicks and moves. protected override void OnMouseDown (MouseButtonEventArgs args) { base.OnMouseDown(args); Point pt = args.GetPosition(canvas); if (args.ChangedButton == MouseButton .Left) ptCtrl1.Center = pt; if (args.ChangedButton == MouseButton .Right) ptCtrl2.Center = pt; } protected override void OnMouseMove (MouseEventArgs args) { base.OnMouseMove(args); Point pt = args.GetPosition(canvas); if (args.LeftButton == MouseButtonState.Pressed) ptCtrl1.Center = pt; if (args.RightButton == MouseButtonState.Pressed) ptCtrl2.Center = pt; } } }



The two end points are always fixed relative to the window. The left mouse button controls the first control point, and the right mouse button controls the second control point. As you experiment with this program, you'll find that with a little practice you can manipulate the curve into something close to the shape you want.

The Bézier spline is very well controlled. Some splines don't pass through any of the points that define them. The Bézier spline is always anchored at the two end points. (As you'll see, this is one of the assumptions used to derive the Bézier formulas.) Some forms of splines have singularities where the curve veers off into infinityan effect rarely desired in computer-design work. The Bézier spline is much better behaved. In fact, the Bézier curve is always bounded by a four-sided polygon (called a convex hull) that is formed by connecting the end points and the control points. (The way in which you connect the end points and the control points to form this convex hull depends on the particular curve.)

At the start point, the curve is always tangential to and in the same direction as a straight line drawn from the start point to the first control point. (This relationship is visually illustrated in the BezierExperimenter program.) At the end point, the curve is always tangential to and in the same direction as a straight line drawn from the second control point to the end point. These are actually two assumptions used to derive the Bézier formulas.

Apart from the mathematical characteristics, the Bézier curve is often aesthetically pleasing, which is the primary reason that it's found such extensive applications in computer-design work.

You can define multiple connected Bézier splines by setting the Points property of the PolyBezierSegment element. Although there are no restrictions on the number of points PolyBezierSegment is given, it really only makes sense if the number of points is a multiple of three. The first and second points are control points, and the third point is the end point. The third point is also the start point of the second Bézier curve (if any), and the fourth and fifth points are control points for that second Bézier.

Although connected Bézier curves share end points, it's possible that the point at which one curve ends and the next one begins won't be smooth. Mathematically, the composite curve is considered smooth only if the first derivative of the curve is continuousthat is, it doesn't make any sudden changes.

When you draw multiple Bézier curves, you might want the resultant composite curve to be smooth where one curve ends and the next one begins. Then again, you might not. It depends on what you're drawing. If you want two connected Bézier curves to join each other smoothly, the second control point of the first Bézier, the end point of the first Bézier (which is also the start point of the second Bézier), and the first control point of the second Bézier must be colinearthat is, lie on the same line.

Here's a little XAML file that demonstrates a standard technique to simulate a circle with four connected Bézier splines. These end points and control points have the colinearity required for a smooth curve.

SimulatedCircle.xaml

[View full width]

<!-- === =============================================== SimulatedCircle.xaml (c) 2006 by Charles Petzold ============================================= ===== --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Path Canvas.Left="150" Canvas.Top="150" Stroke="Black"> <Path.Data> <PathGeometry> <PathGeometry.Figures> <PathFigure StartPoint="0 100"> <PolyBezierSegment Points=" 55 100, 100 55, 100 0 100 -55, 55 -100, 0 -100 -55 -100, -100 -55, -100 0 -100 55, -55 100, 0 100" /> </PathFigure> </PathGeometry.Figures> </PathGeometry> </Path.Data> </Path> </Canvas>



The simulated circle as defined by the Points array has a radius of 100 and is centered around the point (0, 0). The Left and Top attached properties of Canvas move the Path to a more visible location. The following program is a little variation of the simulated circle and draws an infinity sign with a linear gradient brush.

Infinity.xaml

[View full width]

<!-- =========================================== Infinity.xaml (c) 2006 by Charles Petzold =========================================== --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Path Canvas.Left="150" Canvas.Top="150" StrokeThickness="25"> <Path.Stroke> <LinearGradientBrush> <LinearGradientBrush.GradientStops> <GradientStop Offset="0.00" Color="Red" /> <GradientStop Offset="0.16" Color="Orange" /> <GradientStop Offset="0.33" Color="Yellow" /> <GradientStop Offset="0.50" Color="Green" /> <GradientStop Offset="0.67" Color="Blue" /> <GradientStop Offset="0.84" Color="Indigo" /> <GradientStop Offset="1.00" Color="Violet" /> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Path.Stroke> <Path.Data> <PathGeometry> <PathGeometry.Figures> <PathFigure StartPoint="0 -100"> <PolyBezierSegment Points=" -55 -100, -100 -55, -100 0, -100 55, -55 100, 0 100, 55 100, 100 50, 150 0, 200 -50, 245 -100, 300 -100, 355 -100, 400 -55, 400 0, 400 55, 355 100, 300 100, 245 100, 200 50, 150 0, 100 -50, 55 -100, 0 -100" /> </PathFigure> </PathGeometry.Figures> </PathGeometry> </Path.Data> </Path> </Canvas>



It's sometimes helpful to know the underlying mathematics that a graphics system uses to render particular curves, and even to actually derive the curves, if only so that you don't think the formulas just fell out of the sky one day.

The cubic form of the Bézier spline is uniquely defined by four points, which I've called p0 (the begin point), p1 and p2 (the two control points), and p3 (the end point). These four points can also be denoted as (x0, y0), (x1, y1), (x2, y2), and (x3, y3).

The general parametric form of a cubic polynomial in two dimensions is:

x(t)=ax.t3+bx.t2+cx.t+dx

y(t)=ay.t3+by.t2+cy.t+dy

where ax, bx, cx, dx, ay, by, cy, and dy are constants, and t ranges from 0 to 1. Every Bézier spline is uniquely defined by these eight constants. The constants are dependent on the four points that define the curve. The object of this exercise is to derive the values of the eight constants in terms of the four points.

The first assumption is that the Bézier spline begins at the point (x0, y0) when t equals 0:

x(0)=x0

y(0)=y0

Even with this simple assumption we can make some headway in deriving the constants. If you put a 0 value for t in the parametric equations, you get:

x(0)=dx

y(0)=dy

That means that two of the constants are simply the coordinates of the start point:

1a.


1b.


The second assumption regarding the Bézier spline is that it ends at the point (x3, y3) when t equals 1:

x(1)=x3

y(1)=y3

Substituting a value of 1 for t in the parametric equations yields the following:

x(1)=ax+bx+cx+dx

y(1)=ay+by+cy+dy

This means that the constants relate to the coordinate of the end point like so:

2a.


2b.


The remaining two assumptions involve the first derivative of the parametric equations, which describe the slope of the curve. The first derivatives of the generalized parametric equations of a cubic polynomial with respect to t are:

x'(t)=3axt2+2 bxt+cx

y'(t)=3ayt2+2 byt+cy

In particular we're interested in the slope of the curve at the two end points. At the start point the Bézier curve is tangential to and in the same direction as a straight line drawn from the start point to the first control point. That straight line would normally be defined by the parametric equations:

x(t)=(x1-x0)t+x0

y(t)=(y1-y0)t+y0

for t ranging from 0 to 1. However, another way of expressing this straight line would be the parametric equations:

x(t)=3(x1-x0)t+x0

y(t)=3(y1-y0)t+y0

where t ranges from 0 to 1/3. Why 1/3? Because the section of the Bézier curve that is tangential to and in the same direction as the straight line from p0 to p1 is roughly 1/3 of the total Bézier curve. Here are the first derivatives of those revised parametric equations:

x'(t)=3(x1-x0)

y'(t)=3(y1-y0)

We want these equations to represent the slope of the Bézier spline when t equals 0, so:

x'(0)=3(x1-x0)

y'(0)=3(y1-y0)

Substitute 0 for t in the generalized cube first derivatives and you get:

x'(0)=cx

y'(0)=cy

That means

3a.


3b.


The final assumption is that at the end points, the Bézier curve is tangential to and in the same direction as a straight line from the second control point to the end point. In other words

x'(1)=3(x3-x2)

y'(1)=3(y3-y2)

Since we know from the generalized formulas that

x'(1)=3ax+2bx+cx

y'(1)=3ay+2by+cy

Then

4a.


4b.


Equations 1a, 2a, 3a, and 4a provide four equations and four unknowns that let you solve for ax, bx, cx, and dx in terms of x0, x1, x2, and x3. Go through the algebra and you'll find:

ax=-x0+3x1-3x2+x3

bx=3x0-6x1+3x2

cx=3x0+3x1

dx=x0

Equations 1b, 2b, 3b, and 4b let us do the same for the y coefficients. We can then put the constants back into the generalized cubic parametric equations:

x(t)=(-x0+3x1-3x2+x3)t3+(3x0+6x1+3x2)t2+(3x0+3x1)t+x0

y(t)=(-y0+3y1-3y2+y3)t3+(3y0+6y1+3y2)t2+(3y0+3y1)t+y0

We're basically done. However, it's much more common for the terms to be rearranged to yield the more elegant and easier-to-use parametric equations:

x(t)=(1-t)3x0+3t(1-t)2x1+3t2(1-t)x2+t3x3

y(t)=(1-t)3y0+3t(1-t)2y1+3t2(1-t)y2+t3xy3

These equations are the customary form in which the Bézier spline is expressed. Each point on the curve is a weighted average of the four points that define the curve. It's fairly easy to demonstrate that the equations I've derived match the algorithm that the WPF uses. The BezierReproduce project includes both BezierExperimenter.xaml and BezierExperimenter.cs and the following file, which includes a class that inherits from BezierExperimenter to draw a blue Polyline with calculated points for the Bézier spline.

BezierReproduce.cs

[View full width]

//------------------------------------------------ // BezierReproduce.cs (c) 2006 by Charles Petzold //------------------------------------------------ using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; namespace Petzold.BezierReproduce { public class BezierReproduce : Petzold .BezierExperimenter.BezierExperimenter { Polyline bezier; [STAThread] public new static void Main() { Application app = new Application(); app.Run(new BezierReproduce()); } public BezierReproduce() { Title = "Bezier Reproduce"; bezier = new Polyline(); bezier.Stroke = Brushes.Blue; canvas.Children.Add(bezier); } protected override void CanvasOnSizeChanged(object sender, SizeChangedEventArgs args) { base.CanvasOnSizeChanged(sender, args); DrawBezierManually(); } protected override void OnMouseDown (MouseButtonEventArgs args) { base.OnMouseDown(args); DrawBezierManually(); } protected override void OnMouseMove (MouseEventArgs args) { base.OnMouseMove(args); DrawBezierManually(); } void DrawBezierManually() { Point[] pts = new Point[10]; for (int i = 0; i < pts.Length; i++) { double t = (double)i / (pts.Length - 1); double x = (1 - t) * (1 - t) * (1 - t) * ptStart.Center.X + 3 * t * (1 - t) * (1 - t) * ptCtrl1.Center.X + 3 * t * t * (1 - t) * ptCtrl2.Center.X + t * t * t * ptEnd.Center.X; double y = (1 - t) * (1 - t) * (1 - t) * ptStart.Center.Y + 3 * t * (1 - t) * (1 - t) * ptCtrl1.Center.Y + 3 * t * t * (1 - t) * ptCtrl2.Center.Y + t * t * t * ptEnd.Center.Y; pts[i] = new Point(x, y); } bezier.Points = new PointCollection(pts); } } }



Although the blue Polyline has only ten pointsit really looks more like a polyline than a curvethe match is very good.

The Windows Presentation Foundation also supports a so-called quadratic Bézier curve, which has one control point rather than two. When drawing many curves, the quadratic curve is more efficient than the cubic Bézier. The QuadraticBezierSegment class has a Point1 property, which is the control point, and Point2, which is the end point. The PolyQuadraticBezierSegment class has a Points property that makes sense only when set to an even number of pointsalternating control points and end points.

If the beginning point of the quadratic spline is (x0, y0), the control point is (x1, y1), and the end point is (x2, y2), the parametric formulas for the quadratic Bézier are

x(t)=(1-t)2x0+2t(1-t)x1+t2x2

y(t)=(1-t)2y0+2t(1-t)y1+t2y2

How different is this curve from a cubic Bézier when both control points of the cubic Bézier are the same and equal to the single control point of the quadratic Bézier? The quadratic Bézier is roughly the average between the cubic Bézier and a straight line connecting the end points. It's actually possible to convince yourself of this relationship without looking at any curves!

Consider a straight line between (x0, y0) and (x2, y2). What is the midpoint of that line? It's this:


What is the midpoint of the quadratic Bézier curve that begins and ends at the same point with a control point of (x1, y1)? Substitute 0.5 for t in the quadratic formulas and you get


and similarly for y. It's a weighted average of the three points. Substitute 0.5 for t in the cubic Bézier formulasand adjust the terminology so the two control points are both (x1, y1), and the end point is (x2, y2)and you'll derive


The cubic Bézier places a greater weight on the control point and lesser weight on the end points than the quadratic curve. The average of the midpoint of the cubic Bézier and the straight line is


That's close to the midpoint of the quadratic curve.

Now that you have been introduced to all the PathSegment derivatives, it's time to unveil the path mini-language (more officially known as the PathGeometry Markup Syntax), which is a string encompassing all the types of path segments. You can set this string directly to the Data attribute of Path and any other property of type Geometry that you might encounter in other elements.

Let's look at a little example first.

PathMiniLanguage.xaml

[View full width]

<!-- === ================================================ PathMiniLanguage.xaml (c) 2006 by Charles Petzold ============================================= ====== --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Path Fill="Aqua" Stroke="Magenta" StrokeThickness="3" Data="M 50 75 L 250 75, 250 275, 50 275 Z" /> </Canvas>



The mini-language alternates letter commands with numeric parameters. The M stands for move, and it's the equivalent of the StartPoint property of PathFigure. The point is (50, 75). The L is line, of coursethe three points that follow the L cause lines to be drawn from (50, 75) to (250, 75) to (250, 275) to (50, 275). The Z at the end doesn't stand for anything, except that as the last letter of the alphabet, it serves to close the figure (which is a 200-unit square). Another M command can follow to begin a new figure, or a new figure can implicitly begin at (50, 75), which is considered to be the last point of the figure after the figure is closed. New figures always begin with M or implicitly begin after a Z.

The H and V commands draw horizontal and vertical lines, respectively, to the specified coordinate. The same square could be drawn like this:

Data="M 50 75 H 250 V 275 H 50 Z" 


Lowercase letters generally do the same thing as their uppercase counterparts, except relatively rather than absolutely. This string also creates the same square, but the numbers following h and v indicate lengths rather than coordinates:

Data="M 50 75 h 200 v 200 h -200 Z" 


The line-drawing command also comes in a relative version:

Data="M 50 75 l 200 0, 0 200, -200 0 Z" 


If you want to set the FillRule to NonZero rather than the default EvenOdd, you need to include an F1 at the very beginning. (F0 just means the default.)

The complete mini-language is described in the following table. In this table, the point (x0, y0) is the current point, which is initially the point set by the move command, and subsequently is the last point of the previous drawing command.

Command

Name

Description

F i

FillRule

i=0:EvenOdd.

i=1:NonZero.

M x y

Move

Moveto (x, y).

m x y

Relative move

Moveto (x0+x, y0+y).

L x y

Line

Drawline to (x, y).

l x y

Relative line

Drawline to (x0+x, y0+y).

H x

Horizontal line

Drawline to (x, y0).

h x

Relative horizontal line

Drawline to (x0+x, y0).

V y

Vertical line

Drawline to (x0, y).

v y

Relative vertical line

Drawline to (x0, y0+y).

A xr yr a i j x y

Arc

Drawarc to (x, y) based on ellipse with radii (xr,yr) rotated a degrees. i=1:IsLargeArc. j=1: Clockwise.

a xr yr a i j x y

Relative arc

Drawarc to (x0+x, y0+y).

C x1 y1 x2y2 x3 y3

CubicBézier

DrawBézier to (x3, y3) withcontrol points (x1, y1) and (x2, y2).

c x1 y1 x2y2 x3 y3

Relative cubic Bézier

DrawBézier to (x0+x3,y0+y3) with control points (x0+x1, y0+y1) and (x0+x2, y0+y2).

S x2 y2 x3 y3

Smooth cubic Bézier

DrawBézier to (x3, y3) with reflected control point and (x2, y2).

s x2 y2 x3y3

Relative smooth cubic Bézier

DrawBézier to (x0+x3,y0+y3) with reflected control point and (x0+x2, y0+y2).

Q x1 y1 x2y2

Quadratic Bézier

Drawquadratic Bézier to (x2, y2)with control point (x1, y1).

q x1 y1 x2y2

Relative quadratic Bézier

Drawquadratic Bézier to (x0+x2,y0+y2) with control point(x0+x1, y0+y1).

Z

z

Closefigure

 


The smooth cubic Bézier ensures that the curve is connected smoothly to the previous curve by calculating the first control point. Thus the start point of the Bézier curve is the midpoint of a straight line from the second control point of the previous Bézier curve to the calculated point. That's ideal for the infinity sign, as demonstrated here.

MiniLanguageInfinity.xaml

[View full width]

<!-- === ==================================================== MiniLanguageInfinity.xaml (c) 2006 by Charles Petzold ============================================= ========== --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Path Canvas.Left="150" Canvas.Top="150" Stroke="Black" Data="M 0 -100 C -55 -100, -100 -55, -100 0 S -55 100, 0 100 S 100 50, 150 0 S 245 -100, 300 -100 S 400 -55, 400 0 S 355 100, 300 100 S 200 50, 150 0 S 55 -100, 0 -100" /> </Canvas>



You use geometries not only for drawing but for clipping. UIElement defines a property named Clip of type Geometry that you can set to any Geometry object, or you can set Clip directly to a path mini-language string directly in XAML. Any part of the geometry that would not normally be filled if the geometry were being drawn is not displayed by the element.

For example, the following XAML file sets the Clip property of a button to a mini-language string that defines four arcs. The first two effectively define an ellipse the width and height of the button, and the second defines a little circle within that ellipse.

ClippedButton.xaml

[View full width]

<!-- ================================================ ClippedButton.xaml (c) 2006 by Charles Petzold ============================================= === --> <Grid xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <Button HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="24" Width="200" Height="100" Clip="M 0 50 A 100 50 0 0 0 200 50 A 100 50 0 0 0 0 50 M 90 50 A 10 10 0 0 0 110 50 A 10 10 0 0 0 90 50" > Clipped Button </Button> </Grid>



The following program displays a famous image from the NASA Web site and clips it to an outline of a keyhole.

KeyholeOnTheMoon.xaml

[View full width]

<!-- === ================================================ KeyholeOnTheMoon.xaml (c) 2006 by Charles Petzold ============================================= ====== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Image Source="http://images.jsc.nasa.gov /lores/AS11-40-5903.jpg" Clip="M 300 130 L 250 350 L 450 350 L 400 130 A 70 70 0 1 0 300 130" Stretch="None" /> </Page>



The outlines of the fonts that we use under Windows are defined by cubic and quadratic Bézier curves. Displayable font characters are created from these curves in a process called rasterization. These curves are not used blindly in this process, however. The font files contain "hints" so that rounding errors inherent in rasterization do not make the font characters unreadable at the small sizes common on video displays.

It's possible, however, to obtain font character outlines in the form of unhinted geometries. You can then use these outlines for certain graphical techniques that might not be possible otherwise.

You'll recall the FormattedText class, I hope. You use that class to prepare text for the DrawText method of the DrawingContext class. That class has a BuildGeometry method that returns an object of type Geometry containing the character outlines of the text. However, some problems exist with using the FormattedText class directly in XAML. You could certainly create a FormattedText object as a resource, but BuildGeometry is a method rather than a property, so it wouldn't be accessible by XAML elements.

Another approach you might consider is creating a class that derives from Geometry. Such a class could be used wherever any other Geometry derivative appears. This, too, is impossible. Not enough information is documented to successfully inherit from Geometry itself, and all the other classes that inherit from Geometry are sealed.

After encountering these obstacles I wrote the following simple class that has properties that parallel the arguments required for the FormattedText constructor, and another property named Geometry that calls the BuildGeometry method on the FormattedText object.

TextGeometry.cs

[View full width]

//--------------------------------------------- // TextGeometry.cs (c) 2006 by Charles Petzold //--------------------------------------------- using System; using System.Globalization; using System.Windows; using System.Windows.Media; namespace Petzold.TextGeometryDemo { public class TextGeometry { // Private fields backing public properties. string txt = ""; FontFamily fntfam = new FontFamily(); FontStyle fntstyle = FontStyles.Normal; FontWeight fntwt = FontWeights.Normal; FontStretch fntstr = FontStretches.Normal; double emsize = 24; Point ptOrigin = new Point(0, 0); // Public Properties. public string Text { set { txt = value; } get { return txt; } } public FontFamily FontFamily { set { fntfam = value; } get { return fntfam; } } public FontStyle FontStyle { set { fntstyle = value; } get { return fntstyle; } } public FontWeight FontWeight { set { fntwt = value; } get { return fntwt; } } public FontStretch FontStretch { set { fntstr = value; } get { return fntstr; } } public double FontSize { set { emsize = value; } get { return emsize; } } public Point Origin { set { ptOrigin = value; } get { return ptOrigin; } } // Public read-only property to return Geometry object. public Geometry Geometry { get { FormattedText formtxt = new FormattedText(Text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface (FontFamily, FontStyle, FontWeight, FontStretch), FontSize, Brushes.Black); return formtxt.BuildGeometry(Origin); } } // Required for animations using paths. public PathGeometry PathGeometry { get { return PathGeometry .CreateFromGeometry(Geometry); } } } }



This class certainly isn't very sophisticated. It doesn't have any dependency properties, but all I really wanted (for the moment) was something I could create as a resource and assign various properties. Then I could use the Geometry property as a one-time binding source.

The XAML file that makes use of this class is shown next. Sure enough, it creates two TextGeometry objects as resources. One contains the word "Hollow" and the other contains the word "Shadow," both with a 144-point Times New Roman Bold font.

TextGeometryWindow.xaml

[View full width]

<!-- === ================================================== TextGeometryWindow.xaml (c) 2006 by Charles Petzold ============================================= ======== --> <Window xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" xmlns:src="/books/4/266/1/html/2/clr-namespace:Petzold .TextGeometryDemo" Title="TextGeometry Demo"> <Window.Resources> <src:TextGeometry x:Key="txtHollow" Text="Hollow" FontFamily="Times New Roman" FontSize="192" FontWeight="Bold" /> <src:TextGeometry x:Key="txtShadow" Text="Shadow" FontFamily="Times New Roman" FontSize="192" FontWeight="Bold" /> </Window.Resources> <TabControl> <TabItem Header="Hollow"> <Path Stroke="Blue" StrokeThickness="5" Data="{Binding Source={StaticResource txtHollow}, Path=Geometry}" /> </TabItem> <TabItem Header="Dotted"> <Path Stroke="Blue" StrokeThickness="5" StrokeDashArray="{Binding Source={x:Static DashStyles.Dot}, Path=Dashes}" StrokeDashCap="Round" Data="{Binding Source={StaticResource txtHollow}, Path=Geometry}" /> </TabItem> <TabItem Header="Shadow"> <Canvas> <Path Fill="DarkGray" Data="{Binding Source={StaticResource txtShadow}, Path=Geometry}" Canvas.Left="12" Canvas .Top="12" /> <Path Stroke="Black" Fill="White" Data="{Binding Source={StaticResource txtShadow}, Path=Geometry }" /> </Canvas> </TabItem> </TabControl> </Window>



The window contains a TabControl with three tabs that reveal what the program does with the Geometry property of the TextGeometry objects. The first TabItem simply strokes the path with a blue brush. It sounds simple, but it creates outlined font characters, which as far as I know aren't available in WPF any other way.

The second TabItem does something similar but draws the outline with a dotted line. The third TabItem draws text with a drop shadow, but the foreground text has a white interior and is outlined with black. The following application-definition file completes the project.

TextGeometryApp.xaml

[View full width]

<!-- === =============================================== TextGeometryApp.xaml (c) 2006 by Charles Petzold ============================================= ===== --> <Application xmlns="http://schemas.microsoft.com/ winfx/2006/xaml/presentation" StartupUri="TextGeometryWindow.xaml" />



When you use these text outlines, it's important to keep the text reasonably large. Remember that these are only the bare, unhinted outlines, and they will degenerate considerably at smaller sizes.




Applications = Code + Markup. A Guide to the Microsoft Windows Presentation Foundation
Applications = Code + Markup: A Guide to the Microsoft Windows Presentation Foundation (Pro - Developer)
ISBN: 0735619573
EAN: 2147483647
Year: 2006
Pages: 72

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net