はじめに
ドラゴン曲線というフラクタル曲線があります。
その中でも有名なのはヘイウェイドラゴンという曲線です。
シンプルな規則からは想像できない複雑で有機的な形が現れます。
この“生き物のような感じ”をもっと強調できないかと思い、折れ線の角度を変えながら描画するアニメーションを作ってみました。
ヘイウェイドラゴンの構造を保ったまま、回転によって形が変化していく様子を紹介します。
成果物
動きをつけてみたのがこれです。
なかなか面白い動きになったのではないかと思います。
ここから、作り方について解説していきましょう。
作り方の説明
ヘイウェイドラゴンの描き方
ヘイウェイドラゴンの作り方はシンプルです。線分を斜め45度の直角な折れ線に変換することを繰り返していきます。
45度の三角定規の長辺を残り2辺に変換する、と言えばいいでしょうか。
アニメーションにするとこんな感じです。
ひとつ手前の世代の曲線はピンク色の補助線をつけています。
角度を変えてみる
今回考えたのは、斜め45度以外の折れ線に変化させるというアプローチです。
45度の三角定規だけでなく、30度の三角定規、そのほかの直角三角形でもやってみようじゃないかということです。
なぜ直角三角形にこだわるかというのは後述します。
折れ線の角度自体は90度です。斜め30度の直角な折れ線に変換する場合はこんな感じです。
折れ線の両端は固定で、中間の点だけが直角を保ちながら動きます。
起終点が固定で中間の点が直角ならば、その点は円周を描きます。この方法なら、安定した大きさで動きをつけられるのではないかと考えました。
ここでちょっと面白いのが1回転が180度ということです。360度ではありません。
ある点から右側に伸ばしたラインの角度なので、180度に収まります。さらにすべての角度は円に投影できるので回転して見えるのです。
イメージはこんな感じです。processingはY軸が下向きですが、分かりやすさのために上向き、つまり通常のXY座標系で表現しています。
| 直角の回転のイメージ | 1つの角度に着目したイメージ |
|---|---|
![]() |
![]() |
折れ線の回転による変化
アニメーションの前に、いくつか面白い事例をご紹介します。
名前は勝手につけました。
![]() トルクスパイラル(15度) |
![]() ジャイアント曲線(30度) |
|---|---|
![]() ヘイウェイドラゴン(45度) |
![]() テンタクル(60度) |
さて、本題のアニメーションです。ここでもう一度載せておきます。
おわりに
今回は予想がうまく的中して、なかなか面白い動きが作れたと思います。
今後は、世代ごとに角度を変えてみるとか、折れ線を2辺から3辺以上にするとかやってもおもしろいかもしれません。
3次元化、色を変えたりするのも良いかもしれませんね。
最後に、今回のアニメーションを描画するためのコードを記載しておきます。
enum Mode { MEASURE, DRAW }
// 描画範囲のクラス
class Bounds {
PVector minVec = new PVector( Float.MAX_VALUE, Float.MAX_VALUE);
PVector maxVec = new PVector(-Float.MAX_VALUE, -Float.MAX_VALUE);
void update(PVector vec) {
minVec.x = min(minVec.x, vec.x);
minVec.y = min(minVec.y, vec.y);
maxVec.x = max(maxVec.x, vec.x);
maxVec.y = max(maxVec.y, vec.y);
}
}
Bounds drawBounds = new Bounds();
int waveMax = 16; // 分割回数
int angle = 0; // 折れ線の方向
float ratio = 0.0; // 折れ線の縮小率
int lineLength = 400; // オリジナルのラインの長さ
int margin = 50; // 描画範囲の余白
PVector orgVec1 = new PVector(0, 0); // オリジナルのラインの起点
PVector orgVec2 = new PVector(lineLength, 0); // オリジナルのラインの終点
void setup() {
// 0~179度までの描画範囲範囲を計算する
for(int i=0; i<180; i++){
angle = i;
ratio = cos(radians(angle));
divideLine(orgVec1, orgVec2, ratio, 0, Mode.MEASURE);
}
// 計算した描画範囲を元にキャンバスの大きさを変える
int drawWidth = int(drawBounds.maxVec.x - drawBounds.minVec.x) + margin * 2;
int drawHeight = int(drawBounds.maxVec.y - drawBounds.minVec.y) + margin * 2;
surface.setSize(drawWidth, drawHeight);
smooth();
background(255);
strokeWeight(1.5);
frameRate(30);
}
void draw() {
translate(-drawBounds.minVec.x+margin, -drawBounds.minVec.y+margin);
background(255);
angle = frameCount-1;
ratio = cos(radians(angle));
stroke(255, 0, 0); // 赤色
PVector vec1 = new PVector(0,0);
PVector vec2 = new PVector(lineLength, 0);
divideLine(vec1, vec2, ratio, 0, Mode.DRAW);
// 保存(必要なら)
saveFrame("img"+nf(frameCount-1,3)+".png");
if (frameCount >= 180) {
noLoop();
}
}
// 曲線の分割関数(再帰処理)
void divideLine(PVector vec1, PVector vec2, float ratio, int waveNum, Mode mode) {
// 指定した分割回数に届いたとき、描画モードならラインを引き、描画範囲の計測モードなら範囲の更新をして終わり
if (waveNum == waveMax) {
if(mode == Mode.DRAW){
line(vec1.x, vec1.y, vec2.x, vec2.y);
return;
}
else{
drawBounds.update(vec1);
drawBounds.update(vec2);
return;
}
}
float nextLen = vec1.dist(vec2) * ratio;
PVector diffVec = PVector.sub(vec2, vec1);
float curDir = atan2(diffVec.y, diffVec.x);
PVector dir = PVector.fromAngle(curDir + radians(angle));
dir.mult(nextLen);
PVector vec3 = vec1.copy();
vec3.add(dir);
divideLine(vec1, vec3, ratio, waveNum + 1, mode);
divideLine(vec2, vec3, ratio, waveNum + 1, mode);
}





