はじめに
コッホ曲線とは、フラクタル図形の一種です。
線分を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次のコッホ曲線を示します。
この記事は、Gushwell's C# Programming Pageで公開したものを大幅に変更・加筆したものです。