Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 102 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
102
Dung lượng
1,69 MB
Nội dung
Chapter 13 Vector Graphics The world of two-dimensional computer graphics is generally divided between vector graphics and raster graphics—a graphics of lines and a graphics of pixels—a graphics of draw programs and a graphics of paint programs—a graphics of cartoons and a graphics of photographs. Vector graphics is the visual realization of analytic geometry. Two-dimensional coordinate points in the form (x, y) define straight lines and curves. In Silverlight, these curves can be arcs on the circumference of an ellipse or Bezier curves, either in the customary cubic form or in a simplified quadratic form. You can “stroke” these lines with a pen of a desired brush, width, and style. A series of connected lines and curves can also define an enclosed area that can be filled with a brush. Raster graphics (which I’ll discuss in the next chapter) involves bitmaps. In Silverlight it is very easy to display a PNG or JPEG file using an Image element as I demonstrated as early as Chapter 4. But as I’ll show you in the next chapter, it’s also possible to generate bitmaps algorithmically in code using the WriteableBitmap class. The worlds of raster graphics and vector graphics intersect when an ImageBrush is used to fill an area, or when vector graphics are used to generate an image on a WriteableBitmap. The Shapes Library A Silverlight program that needs to draw vector graphics uses classes defined in the System.Windows.Shapes namespace, commonly referred to as the Shapes library. This namespace consists of an abstract class named Shape and six sealed classes that derive from Shape: Object DependencyObject (abstract) FrameworkElement (abstract) Shape (abstract) Rectangle (sealed) Ellipse (sealed) Line (sealed) Polyline (sealed) Polygon (sealed) Path (sealed) 393 The Shape class derives from FrameworkElement, which means that these objects get touch input, participate in layout, and can have transforms. In Silverlight there is insufficient information to allow you to derive a class from Shape itself. You’ve already seen Rectangle and Ellipse, but these are really two oddball classes in the realm of vector graphics because they don’t contain any coordinate points. You can just stick an Ellipse in a UserControl and it fills the whole control. You can size the element, but positioning it at an arbitrary point requires a Margin or Padding property, or a RenderTransform, or putting it on a Canvas and using the Left and Top attached properties. The other four classes of Shape are different; these allow you to position the elements with actual coordinate points. Although I’ll discuss the Path class last, it is so versatile that it is pretty much the only class you need for all your vector graphics jobs. If you need to draw an arc or a Bezier spline, you’ll be using the Path class. Shape defines 11 settable properties that are inherited by all its descendants: • Fill of type Brush • Stroke of type Brush • StrokeThickness of type double • StrokeStartLineCap and StrokeEndLineCap of type PenLineCap • StrokeLineJoin of type PenLineJoin • StrokeMiterLimit of type double • StrokeDashArray of type DoubleCollection • StrokeDashCap of type PenLineCap • StrokeDashOffset of type double • Stretch property of type Stretch You’ve already seen the first three properties in connection with Rectangle and Ellipse. The Fill property specifies the Brush used to fill the interior of the figure; the Stroke property is the Brush used to color the outline of the figure, and StrokeThickness is the width of that outline. All the other properties can be used with Rectangle and Ellipse as well. Although the two enumerations (PenLineCap and PenLineJoin) allude to a Pen, there is no Pen class in Silverlight. Conceptually, the properties beginning with the word Stroke together comprise an object traditionally regarded as a pen. 394 Canvas and Grid The Line class defines four properties of type double named X1, Y1, X2, and Y2. The line is drawn from the point (X1, Y1) to the point (X2, Y2) relative to its parent: <Canvas Background="LightCyan"> <Line X1="50" Y1="100" X2="200" Y2="150" Stroke="Blue" /> </Canvas> Many of the examples in this program will be shown as a snippet of XAML and the corresponding image in a 480-square pixel area. At the end of the chapter I’ll describe the program that created these images. For the printed page I’ve made the resolution of these images about 240 dots per inch so they are approximately the same size as what you would see on the actual phone. The line begins at the coordinate point (50, 100) and ends at the point (200, 150). All coordinates are relative to an upper-left origin; increasing values of X go from left to right; increasing values of Y go from top to bottom. The X1, Y1, X2, and Y2 properties are all backed by dependency properties so they can be the targets of styles, data bindings, and animations. Although the Canvas panel seems like a natural for vector graphics, you’ll get the same image if you use a single-cell Grid: <Grid Background="LightCyan"> <Line X1="50" Y1="100" X2="200" Y2="150" Stroke="Blue" /> </Grid> 395 Normally when you use a Canvas you use the Canvas.Left and Canvas.Top attached properties to position elements within the Canvas. Those properties are not required with the Line because it has its own coordinates. You could use the attached properties with the Line but the values are compounded with the coordinates: <Canvas Background="LightCyan"> <Line X1="50" Y1="100" X2="200" Y2="150" Canvas.Left="150" Canvas.Top="100" Stroke="Blue" /> </Canvas> Usually when you’re working with elements that indicate actual coordinate positions, you’ll use the Canvas.Left and Canvas.Top attached properties only for special purposes, such as moving an object relative to the Canvas. Moreover, you’ll recall that a Canvas always reports to the layout system that it has a size of zero. If you subject the Canvas to anything other than Stretch alignment, it will shrink into nothingness regardless of its contents. For these reasons, I tend to put my vector graphics in a single-cell Grid rather than a Canvas. If a Grid contains one or more Line elements (or any other coordinate-based elements), it will report a size that comprises the maximum non-negative X coordinate and the maximum non- negative Y coordinate of all its children. This can sometimes seem a little weird. If a Grid contains a Line from (200, 300) to (210, 310), the Line will report an ActualWidth of 210 and an ActualHeight of 310, and the Grid will be 210 pixels wide and 310 pixels tall, even though the rendered Line needs only a tiny corner of that space. (Actually, the Line and the Grid will be at least an extra pixel larger to accommodate the StrokeThickness of the rendered Line.) Coordinates can be negative, but the Grid does not take account of negative coordinates. A negative coordinate will actually be displayed to the left of or above the Grid. I have spent much time thinking about this behavior, and I am convinced it is correct. Overlapping and ZIndex Here are two lines: 396 <Grid Background="LightCyan"> <Line X1="100" Y1="300" X2="200" Y2="50" Stroke="Blue" /> <Line X1="50" Y1="100" X2="300" Y2="200" Stroke="Red" /> </Grid> The second one overlaps the first one. You can see that more clearly if you go beyond the default 1-pixel thickness of the line using StrokeThickness: <Grid Background="LightCyan"> <Line X1="100" Y1="300" X2="200" Y2="50" Stroke="Blue" StrokeThickness="5" /> <Line X1="50" Y1="100" X2="300" Y2="200" Stroke="Red" StrokeThickness="30" /> </Grid> If you would prefer that the blue line be on top of the red line, there are two ways you can do it. You could simply swap the order of the two lines in the Grid: <Grid Background=”LightCyan”> <Line X1="50" Y1="100" X2="300" Y2="200" Stroke="Red" StrokeThickness="30" /> <Line X1="100" Y1="300" X2="200" Y2="50" Stroke="Blue" StrokeThickness="5" /> </Grid> Or, you could set the Canvas.ZIndex property. Although this property is defined by Canvas it works with any type of panel: 397 <Grid Background="LightCyan"> <Line Canvas.ZIndex="1" X1="100" Y1="300" X2="200" Y2="50" Stroke="Blue" StrokeThickness="5" /> <Line Canvas.ZIndex="0" X1="50" Y1="100" X2="300" Y2="200" Stroke="Red" StrokeThickness="30" /> </Grid> Polylines and Custom Curves The Line element looks simple but the markup is a little bloated. You can actually reduce the markup for drawing a single line by switching from the Line to the Polyline: <Grid Background="LightCyan"> <Polyline Points="100 300 200 50" Stroke="Blue" StrokeThickness="5" /> <Polyline Points="50 100 300 200" Stroke="Red" StrokeThickness="30" /> </Grid> The Points property of the Polyline class is of type PointCollection, a collection of Point objects. In XAML you indicate multiple points by just alternating the X and Y coordinates. You can string out the numbers with spaces between them as I’ve done, or you can clarify the markup a little with commas. Some people prefer commas between the X and Y coordinates: <Polyline Points="100,300 200,50" … Others (including me) prefer to separate the individual points with commas: <Polyline Points="100 300, 200 50" The advantage of Polyline is that you can have as many points as you want: 398 <Grid Background="LightCyan"> <Polyline Points="100 300, 200 50, 350 100, 200 250" Stroke="Blue" StrokeThickness="5" /> <Polyline Points=" 50 100, 300 200, 300 400" Stroke="Red" StrokeThickness="30" /> </Grid> Each additional point increases the total polyline by another line segment. The Polyline does have one significant disadvantage that Line doesn’t have: Because you’re now dealing with a collection of Point objects, the individual points can’t be targets of a style, or a data binding, or an animation. This is not to say that you can’t change the PointCollection at runtime and have that change reflected in the rendered Polyline. You surely can, as I’ll demonstrate in the GrowingPolygons program later in this chapter. Although the Polyline can draw some simple connected lines, it tends to feel underutilized if it’s not fulfilling its true destiny of drawing complex curves, usually generated algorithmically in code. The Polyline is always a collection of straight lines, but if you make those lines short enough and numerous enough, the result will be indistinguishable from a curve. For example, let’s suppose you want to use Polyline to draw a circle. Commonly, a circle centered at the point (0, 0) with a radius R is defined as all points (x, y) that satisfy the equation: This is also, of course, the Pythagorean Formula. But when generating points to draw a graphical circle, this formula tends to be a little clumsy: You need to pick values of x between –R and R, and then solve for y (keeping in mind that most values of x correspond to two values of y) and even if you do this in a systematic manner, you’re going to get a higher density of points in the region where x is close to 0 than the region where y is close to 0. A much better approach for computer graphics involves parametric equations, where both x and y are functions of a third variable, sometimes called t to suggest time. In this case that third variable is simply an angle ranging from 0 to 360°. Suppose the circle is centered on the point (0, 0) and has a radius of R. The circle will be enclosed within a box where values of x go from –R on the left to +R on the right. In keeping 399 with the Silverlight convention that increasing values of y go down, values of y range from –R on the top to +R on the bottom. Let’s begin with an angle of 0° at the rightmost edge of the circle, which is the point (R, 0), and let’s go clockwise around the circle. As the angle goes from 0° to 90°, x goes from R to 0, and then x goes to –R at 180° and then goes back down to zero at 270° and back to R at 360°. This is a familiar pattern: At the same time, the values of y go from 0 to R to 0 to –R and back to 0, or Depending where the circle begins, and in what direction you go, you could have slightly different formulas where the sine and cosine functions are switched, or one or both or negative. If you use different values of R for the two formulas, you’ll draw an ellipse. If you want the circle centered at the point (C x , C y ), you can add these values to the previous results: In a program, you put those two formulas in a for loop that increments an angle value ranging from 0 to 360 to generate a collection of points. How much granularity is required to make the resultant circle look smooth? In this particular example, it depends on the radius. The circumference of a circle is 2πR, so if the radius is 240 pixels (for example), the circumference is approximately 1,500 pixels. Divide by 360° and you get about 4, which means that if you increment the angle in the for loop by 0.25°, the resultant points will be about a pixel apart. (You’ll see later in this chapter that you can get by with a lot fewer points.) Let’s create a new projecvt. Bring up the MainPage.cs file and install a handler for the Loaded event to allow accessing the dimensions of the ContentPanel grid. Here are calculations for center and radius for a circle to occupy the center of a content panel and reach to its edges: Point center = new Point(ContentPanel.ActualWidth / 2, ContentPanel.ActualHeight / 2 - 1); double radius = Math.Min(center.X - 1, center.Y - 1); Notice the pixel subtracted from the calculation of the radius. This is to prevent the circle from being geometrically the same as the content area size. The stroke thickness straddles the geometric line so it would otherwise get cropped off at the edges. 400 Now create a Polyline and set the Stroke and StrokeThickness properties: Polyline polyline = new Polyline(); polyline.Stroke = this.Resources["PhoneForegroundBrush"] as Brush; polyline.StrokeThickness = (double)this.Resources["PhoneStrokeThickness"]; Calculate the Point objects in a for loop based on the formulas I’ve just showed you and add them to the Points collection of the polyline: for (double angle = 0; angle < 360; angle += 0.25) { double radians = Math.PI * angle / 180; double x = center.X + radius * Math.Cos(radians); double y = center.Y + radius * Math.Sin(radians); polyline.Points.Add(new Point(x, y)); } Now add the Polyline to the Grid: ContentPanel.Children.Add(polyline); And here’s the result: So big deal. We created a circle a hard way rather than an easy way. And it’s not even a complete circle: Because the angle in the for loop didn’t go all the way to 360, there’s actually a little gap at the right side. But instead of fixing that problem, let’s do something a little different. Let’s make the angle go all the way to 3600: 401 for (double angle = 0; angle < 3600; angle += 0.25) Now the loop will go around the circle 10 times. Let’s use that angle and the original radius value to calculate a scaledRadius: double scaledRadius = radius * angle / 3600; And use that scaledRadius value for multiplying by the sine and cosine values. Now the result is an Archimedian spiral: Here’s the complete class: Silverlight Project: Spiral File: MainPage.xaml.cs (excerpt) public partial class MainPage : PhoneApplicationPage { public MainPage() { InitializeComponent(); Loaded += OnLoaded; } void OnLoaded(object sender, RoutedEventArgs args) { Point center = new Point(ContentPanel.ActualWidth / 2, ContentPanel.ActualHeight / 2 - 1); double radius = Math.Min(center.X - 1, center.Y - 1); 402 [...]... (relative to half the StrokeThickness) but you can make it longer if you want: 4 05 Here are two lines, one thick, one thin overlaying... relative coordinates, for example (0 .5, 0 .5) to specify the center, but look what happens when you try that in this case: ... of 1° will have a miter point over 50 0 pixels long! To avoid this type of weirdness a StrokeMiterLimit property kicks in for extreme cases: The... VerticalAlignment="Center"> 4 25 As was very clear early on in Chapter 8, the RenderTransform does not affect how an element is perceived... UniformToFill Here’s an innocent little Polygon: Now here’s the same Polygon with its Stretch property set to Fill 413 Regardless of the... Geometry object itself: 424 This appears to be exactly the same as the earlier example... of the Path: 423 Notice that the CenterX and CenterY properties of RotateTransform are set to the same... HorizontalAlignment="Center" VerticalAlignment="Center"> Well, that doesn’t look right, either! What happened?... HorizontalAlignment="Center" VerticalAlignment="Center"> Another difference between the RenderTransform property... OnManipulationCompleted(ManipulationCompletedEventArgs args) { if (isDragging) { isDragging = false; args.Handled = true; } else if (isDrawing) { Color clr = Color.FromArgb( 255 , (byte)rand.Next( 256 ), (byte)rand.Next( 256 ), (byte)rand.Next( 256 )); path.Fill = new SolidColorBrush(clr); isDrawing = false; args.Handled = true; } base.OnManipulationCompleted(args); } For the dragging operation, cleanup is simple . <Polyline Points="100 300, 200 50 , 350 100, 200 250 " Stroke="Blue" StrokeThickness=" ;5& quot; /> <Polyline Points=" 50 100, 300 200, 300 400" Stroke="Red". Points=" ;50 230, 240 240, 50 250 " Stroke="Black" /> </Grid> The default value is 10 (relative to half the StrokeThickness) but you can make it longer if you want: 4 05 . StrokeEndLineCap="Round" StrokeLineJoin="Miter" StrokeMiterLimit=" ;50 " /> <Polyline Points=" ;50 230, 240 240, 50 250 " Stroke="Black" /> </Grid> Here are