Posted at

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で公開したものを大幅に変更・加筆したものです。