This post is about how to draw a hyperbolic plane using WPF and GDI+ in C#. The program uses Poincare disc (conformal model) representation. I’ll present a little math background and then the code to draw these two patterns:
2D Hyperbolic geometry use an Euclidean circle to represent a hyperbolic plane. One interesting feature of this presentation is that although the hyperbolic plane is infinite, it’s represented in a finite Euclidean circle. You may have seen some tree structure presentations utilizing such features so user can navigate through a massive tree structure within a circular viewport. Poincare disc model uses arcs of circles, which meet the bounding circle orthogonally, to represent straight lines – yes the curves you see in above patterns are actually straight lines in hyperbolic plane. To illustrate above pictures, the core task is to find such circles. Specifically, the arc decided by an angle q1 and an angle q2 is provided by a circle shown in the following diagram. To find the circle we need to know both the center of the circle (cx, cy) and radius of the circle (r).
Drawing the pattern to the left using WPF
Create a new WPF application and update MainWindow.xaml:
<Window x:Class="HyperbolicGeometry.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="500" Width="525" Loaded="Window_Loaded"> <StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="10,10,10,10"> <Image Name="space" Height="350" Width="350" HorizontalAlignment="Center" /> </StackPanel> </Window>Nothing exciting here, we just created a image holder that we’ll draw on on the Window_Load event handler. The code-behind looks like this:
private static bool flip = true; private void Window_Loaded(object sender, RoutedEventArgs e) { int R = (int)(space.Width / 2); DrawingVisual visual = new DrawingVisual(); using (DrawingContext context = visual.RenderOpen()) { visual.Clip = new EllipseGeometry(new Point(R, R), R, R); context.DrawEllipse(Brushes.Transparent, new Pen(Brushes.Black,1), new Point(R,R),R,R); for (double s = 1.5; s <= 48; s = s * 2) { for (double q1 = 0; q1 < Math.PI * 2; q1 += Math.PI / s) { double q2 = q1 + Math.PI / s; drawLine(R, context, q1, q2); } flip = !flip; } } RenderTargetBitmap bmp = new RenderTargetBitmap(R*2,R*2, 96, 96, PixelFormats.Pbgra32); bmp.Render(visual); space.Source = bmp; } private static void drawLine(int R, DrawingContext context, double q1, double q2) { double f = (q2 - q1) / 2; double dq = Math.Abs(f); double r = R * Math.Tan(dq); double rp = Math.Sqrt(r * r + R * R); double cx = R + rp * Math.Cos(q1 + f); double cy = R + rp * Math.Sin(q1 + f); double beta = Math.PI - dq * 2; double k = Math.PI / 2 + q2; context.DrawEllipse(flip?Brushes.Blue: Brushes.White, new Pen(Brushes.Blue, 1), new Point(cx, cy), r, r); }The algorithm of finding the circles is all in the drawLine method. Window_Load generates a series of angle pairs and feed them to drawLine method to draw lines at different locations. Note the visual.Clip call – we are constraining our painting within the bounding circle. This is necessary because when we call DrawEllipse method in drawLine parts of the ellipses will fall outside of the bounding circle. At last, the flip flag allows to vary between blue color and white color to create the final pattern.
Drawing the pattern to the right using WPF
It’s indeed very easy to modify above implementation to draw the pattern to the right but we’ll go one step further here: Instead of relying on the clipping region, we’ll figure out proper arc starting angle and ending angles so that we only paint the necessary portions:
- Comment out the viual.Clip call – this makes sure we don’t cheat.
- Add a drawArc method. WPF doesn’t have a native draw arc method so we have to fashion one:
- Modify drawLine method. Instead of calling DrawEllipse, call drawArc:
- Compile and run the code again, you should see the pattern to the right.
private static void drawArc(DrawingContext drawingContext, Brush brush, Pen pen, Point start, Point end, Size radius) { PathGeometry geometry = new PathGeometry(); PathFigure figure = new PathFigure(); geometry.Figures.Add(figure); figure.StartPoint = start; figure.Segments.Add(new ArcSegment(end, radius, 0, false, SweepDirection.Clockwise, true)); drawingContext.DrawGeometry(brush, pen, geometry); }
drawArc(context, Brushes.Transparent, new Pen(Brushes.Blue, 2), new Point(cx + r * Math.Cos(k), cy + r * Math.Sin(k)), new Point(cx + r * Math.Cos(k + beta), cy + r * Math.Sin(k + beta)), new Size(r,r));
Above code can be easily modified to work with GDI+. The only place that needs special attention is the way how GDI+ draws arcs is different from how WPF does it. Under GDI+, the drawArc call should look like:
gc.DrawArc(new Pen(new SolidBrush(Color.Blue),2), new Rectangle((int)(cx-r), (int)(cy-r), (int)(r*2), (int)(r*2)), (float)(k * 180 / Math.PI), (float)(beta * 180) / Math.PI));
(where gc is the Graphics instance you use to paint on). Consult .Net documentations for differences how arcs are represented differently in the two frameworks.
No comments:
Post a Comment