C#
observer
フラクタル図形
NGraphics

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

はじめに

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

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

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

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

ソースコードの説明

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

Programクラス

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

KochCurveクラス

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

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

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

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

Drawer クラス

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

EasyCanvas クラス

NGraphicパッケージを利用して、線を描いています。結果はpngファイルにしています。

C#のコード

Program.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

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;
        }

        // 2点の傾斜を求める (ラジアン)
        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;
        }

        // 2点の線分の長さを求める
        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

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

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);
        }
    }
}

結果

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

KochCurve6.png


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