NGraphicsを使ってコッホ曲線を描く


## はじめに

コッホ曲線とは、フラクタル図形の一種です。
線分を3等分し、分割した真ん中の線分を底辺とする正三角形を描く(ただし底辺は消す)ことを繰り返すことによって得られる図形です。
詳しくは[こちら(Wikipoedia)](https://ja.wikipedia.org/wiki/コッホ曲線)をご覧ください。

例えば、以下のような図形です。

<img src="https://qiita-image-store.s3.amazonaws.com/0/29415/edda452d-e898-4964-ced6-e9136800ac11.png" width=40%>


Wikipediaの定義によれば、作図手順を無限に繰り返して得られる図形を言うのだそうですが、無限に繰り返すことはできないので、ここでは、繰り返す回数を指定できるようにしました。

僕のコードでは、それほど待たずにプログラムが終わるのは、 この繰り返し数が 7 くらいまでです。 それ以上大きい値だと、結構待つことになります。

## ソースコードの説明

以下、ソースコードの簡単な説明です。

### Programクラス

プログラムをつかさどるクラス。KochCurveとDrawerを使って、図形を描画しています。
ここで、曲線の次数を与えています。


### KochCurveクラス

中心的なメソッドは、Draw(Point p1, Point p2, int generation) メソッドです。

generationは繰り返し回数で、再帰呼び出しのたびに、-1 され、0 になったら、再帰呼び出しをストップします。

p1,p2は、線分の両端を示します。p1,p2の両端の位置が与えられると、その 3 等分した点と、そこから得られる正三角形の頂点を求め、４本の線分を引きます。ただ、このときに実際に線を引くのではなく、generation を  -1 し、Drawメソッドを再帰呼び出しすることで、図形を描いていきます。

`IObservable<T>`を実装していて、実際に線分を描くのは、generationが 0になったときで、このときに、購読者オブジェクトに通知し、購読者オブジェクト側（Drawerクラス）で実際の描画を行なっています。


### Drawer クラス

描画を受け持つDrawerクラスは、`IObserver<T>`を実装している購読者クラスです。
DragonCurve からの通知を受け取り、線を描くのに、以下のEasyCanvasクラスを利用しています。

### EasyCanvas クラス

[NGraphic](https://github.com/praeclarum/NGraphics)パッケージを利用して、線を描いています。結果はpngファイルにしています。



## C#のコード

### Program.cs


```cs
using System;
using System.Collections.Generic;
using System.Linq;


namespace KochCurve
{
    class Program
    {
        static void Main(string[] args)
        {
            int generation = 6;
            int width = 800;
            int height = 400;
            var hilbert = new KochCurve();
            var drawer = new Drawer(width, height, $"KochCurve{generation}.png");
            hilbert.Subscribe(drawer);
            hilbert.Start(width, height, generation);
        }
    }
}
```

### KochCurve.cs

```cs
using System;
using System.Collections.Generic;
using System.Linq;

namespace KochCurve
{
    // 座標
    public class Point
    {
        public float X { get; set; }
        public float Y { get; set; }
        public Point(float x, float y)
        {
            X = x;
            Y = y;
        }
    }

    // 通知データ 線分を表すクラス
    public class Line
    {
        public Point Start { get; set; }
        public Point End { get; set; }
    }

    // KochCurveクラス
    public class KochCurve : IObservable<Line>
    {


        // コッホ曲線を描く。
        public void Start(int width, int height, int genelation)
        {
            Point p1 = new Point(0, height * 0.7f);
            Point p2 = new Point(width, height * 0.7f);
            Draw(p1, p2, genelation);
            Complete();
        }



        // コッホ曲線を描く下請けメソッド （再帰呼び出しされる）
        // ただし、実際の描画は行わない。購読オブジェクトに知らせるだけ
        private void Draw(Point p1, Point p2, int generation)
        {
            if (generation == 0)
            {
                var info = new Line() { Start = p1, End = p2 };
                Publish(info);
            }
            else
            {
                Point a = new Point((p1.X + (p2.X - p1.X) / 3),
                                    (p1.Y + (p2.Y - p1.Y) / 3));
                Point b = new Point((p2.X - (p2.X - p1.X) / 3),
                                    (p2.Y - (p2.Y - p1.Y) / 3));
                Point c = NextPoint(a, b);
                Draw(p1, a, generation - 1);
                Draw(a, c, generation - 1);
                Draw(c, b, generation - 1);
                Draw(b, p2, generation - 1);
            }
        }

        // 次の点を求める
        private Point NextPoint(Point p1, Point p2)
        {
            double direction = ToDegree(GetSlope(p1, p2));
            Point p3 = new Point(p2.X - p1.X, p2.Y - p1.Y);
            double len = LineLength(p1, p2);
            double nd = (direction + 300) % 360;
            Point p4 = new Point(
                        (float)(Math.Cos(ToRadian(nd)) * len + p1.X),
                        (float)(Math.Sin(ToRadian(nd)) * len + p1.Y));
            return p4;
        }

        // ２点の傾斜を求める （ラジアン）
        private double GetSlope(Point p1, Point p2)
        {
            double vx = p2.X - p1.X;
            double vy = p2.Y - p1.Y;
            double n = Math.Atan(vy / vx);
            if (vx < 0)
                return Math.PI + n;    // 180度を足す
            if (vy < 0)
                return (2 * Math.PI) + n;  // 360度を足す
            return n;

        }

        // ラジアンを度の変換
        private double ToDegree(double rad)
        {
            return rad * 360 / (2 * Math.PI);
        }

        // 度をラジアンに変換
        private double ToRadian(double deg)
        {
            return deg * (2 * Math.PI) / 360;
        }

        // ２点の線分の長さを求める
        private double LineLength(Point p1, Point p2)
        {
            double w = (p2.X - p1.X);
            double h = (p2.Y - p1.Y);
            return Math.Sqrt(w * w + h * h);
        }

        // 終了を通知する
        private void Complete()
        {
            foreach (var observer in _observers)
            {
                observer.OnCompleted();
            }
        }


        // 状況変化を知らせるために購読者に通知する
        private void Publish(Line state)
        {
            foreach (var observer in _observers)
            {
                observer.OnNext(state);
            }
        }

        private List<IObserver<Line>> _observers = new List<IObserver<Line>>();

        public IDisposable Subscribe(IObserver<Line> observer)
        {
            _observers.Add(observer);
            return observer as IDisposable;
        }
    }
}
```

### Drawer.CS

```cs
using NGraphics;
using System;
using System.Collections.Generic;
using System.Linq;

namespace KochCurve
{
    class Drawer : IObserver<Line>
    {
        private EasyCanvas _canvas;

        private string _filepath;

        public Drawer(int w, int h, string filepath)
        {
            _canvas = new EasyCanvas(w, h);
            _filepath = filepath;
        }
        public void OnCompleted()
        {
            _canvas.Write(_filepath);
        }

        public void OnError(Exception error)
        {
            throw new NotImplementedException();
        }

        public void OnNext(Line value)
        {
            _canvas.DrawLine(value.Start.X, value.Start.Y, value.End.X, value.End.Y,
                new Color("#886b3f"));
        }
    }
}
```

### EasyCanvas.cs

```cs
using NGraphics;
using System;
using System.Collections.Generic;
using System.Linq;

namespace KochCurve
{
    class EasyCanvas
    {
        private IImageCanvas canvas;

        static EasyCanvas()
        {
        }

        public EasyCanvas(int width, int height)
        {
            canvas = Platforms.Current.CreateImageCanvas(new Size(width, height), scale: 2);
        }

        public void SetPixel(int x, int y, NGraphics.SolidBrush brush)
        {
            canvas.FillRectangle(x, y, 1, 1, brush);
        }

        public void Write(string path)
        {
            canvas.GetImage().SaveAsPng(path);
        }

        internal void DrawLine(float x1, float y1, float x2, float y2, Color color)
        {
            canvas.DrawLine(x1, y1, x2, y2, color);
        }
    }
}
```

※ ソースコードは、[GitHub](
https://github.com/gushwell/CSharpAlgorithmCollection2)で公開しています。


## 結果

6次のコッホ曲線を示します。

![KochCurve6.png](https://qiita-image-store.s3.amazonaws.com/0/29415/5f800e7c-8016-cec1-9b5f-eef94297339c.png "KochCurve6.png")


---

この記事は、Gushwell's C# Programming Pageで公開したものを大幅に変更・加筆したものです。