Help us understand the problem. What is going on with this article?

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

※ ソースコードは、GitHubで公開しています。

結果

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

KochCurve6.png


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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした