サトクリフの五角形 for p5.js


2018年02月12日
With
サトクリフの五角形 for p5.js はコメントを受け付けていません

p5.jsの触りはじめに大変お世話になった書籍「ジェネラティブ・アート-Processingによる実践ガイド」からの移植シリーズもいよいよラスト。


■ [普及版]ジェネラティブ・アート―Processingによる実践ガイド

この本で紹介されている最後のスケッチが「サトクリフの五角形(Sutcliffe pentagons)」です。

サトクリフの五角形

このフラクタル図形に名を冠しているAlan Sutcliffe氏は、デジタルアートの黎明期にその振興と情報交換の場として英国で設立された「Computer Arts Society」の創設者のおひとり。協会の創設は1968年だそうですから相当に歴史のある団体ですね。Sutcliffe氏自身もプログラマー、数学者、アーティストとして活動しておられたそうですが、2014年に鬼籍に入られたとのこと。この本の原書が出たのは2012年ですから、出版当時にはまだご存命だったのですね。

「サトクリフの五角形」は「五角形の各辺の中点から線を延ばし、伸ばした先の点をつなぐと新たに6つの五角形が生まれる。そのそれぞれについてさらに、各辺の中点から線を延ばしてその先をつなぐことを繰り返す」ことで生成されるフラクタルです。

元となる五角形から2回の繰り返しでこんな感じ。

さらに繰り返していくと、こんな感じになっていきます。美しい。


「ジェネラティブ・アート」では、著者のMatt Pearson氏がこのフラクタル図形に「辺の中点から伸ばす線の長さの変化」と「回転」を加えてアニメーションにしています。Processing向けのコードは、CodeProjectのサイトにテキストと一緒に転載されていましたので、ご興味がある方はどぞ。

・Generative Art – Case Study: Sutcliffe Pentagons(CodeProject)

次のリンクはp5.js用に書き換えたものです。表示色や変化量などをちょっとだけいじっています。

●サトクリフの五角形 powered by p5.js(クリックすると別タブが開きます)

伸びたり縮んだりする動きの生成にはnoise関数が使われています。万華鏡をのぞいているような変化で、ぼんやり見ていると癒やされますね。ちなみにベースとなる図形の頂点数は「_numSides」で定義しているので、ここの数字を変えてやると、三角形、四角形、六角形、八角形などなどでも同様のフラクタルが動きます。ぜひお試しを。

コードは以下のとおりです。

const _maxlevels = 4;
const _numSides = 5;
let pentagon;
let _strutFactor;
let _strutNoise;
function setup() {
	createCanvas(600, 600);
	smooth();
	stroke(210, 210, 255, 20);
	_strutNoise = random(10);
	_strutFactor = (noise(_strutNoise) * 2) - 1;
}
function draw() {
	background(30);
	_strutNoise += 0.01;
	_strutFactor = noise(_strutNoise) * 2;
	pentagon = new FractalRoot(frameCount);
	pentagon.drawShape();
}
class PointObj {
	constructor(ex, why) {
		this.x = ex;
		this.y = why;
	}
}
class FractalRoot {
	constructor(startAngle) {
		this.pointArr = [];
		this.centX = width / 2;
		this.centY = height / 2;
		let angleStep = 360 / _numSides;
		let count = 0;
		for (let i = 0; i < 1024; i += angleStep) {
			this.x = this.centX + (250 * cos(radians(startAngle + i)));
			this.y = this.centY + (250 * sin(radians(startAngle + i)));
			this.pointArr[count] = new PointObj(this.x, this.y);
			count++;
		}
		this.rootBranch = new Branch(0, 0, this.pointArr);
	}
	drawShape() {
		this.rootBranch.drawMe();
	}
}
class Branch {
	constructor(lev, n, points) {
		this.myBranches = [];
		this.newPoints = [];
		this.level = lev;
		this.num = n;
		this.outerPoints = points;
		this.midPoints = this.calcMidPoints();
		this.projPoints = this.calcStrutPoints();
		if ((this.level + 1) < _maxlevels) {
			this.childBranch = new Branch(this.level + 1, 0, this.projPoints);
			this.myBranches = append(this.myBranches, this.childBranch);
			for (let k = 0; k < this.outerPoints.length; k++) {
				let nextk = k - 1;
				if (nextk < 0) { nextk += this.outerPoints.length; }
				this.newPoints = [this.projPoints[k], this.midPoints[k], this.outerPoints[k], this.midPoints[nextk], this.projPoints[nextk]];
				this.childBranch = new Branch(this.level + 1, k + 1, this.newPoints);
				this.myBranches = append(this.myBranches, this.childBranch);
			}
		}
	}
	drawMe() {
		strokeWeight(6 - this.level);
		for (let i = 0; i < this.outerPoints.length; i++) {
			let nexti = i + 1;
			if (nexti == this.outerPoints.length) { nexti = 0; } {
				line(this.outerPoints[i].x, this.outerPoints[i].y, this.outerPoints[nexti].x, this.outerPoints[nexti].y);
			}
		}
		strokeWeight(0.5);
		fill(255, 150);
		for (let j = 0; j < this.midPoints.length; j++) {
			line(this.midPoints[j].x, this.midPoints[j].y, this.projPoints[j].x, this.projPoints[j].y);
		}
		for (let k = 0; k < this.myBranches.length; k++) {
			this.myBranches[k].drawMe();
		}
	}
	calcMidPoints() {
		this.mpArray = [];
		for (let i = 0; i < this.outerPoints.length; i++) {
			let nexti = i + 1;
			if (nexti == this.outerPoints.length) { nexti = 0; }
			let thisMP = this.calcMidPoint(this.outerPoints[i], this.outerPoints[nexti]);
			this.mpArray[i] = thisMP;
		}
		return this.mpArray;
	}
	calcMidPoint(end1, end2) {
		let mx, my;
		if (end1.x > end2.x) {
			mx = end2.x + ((end1.x - end2.x) / 2);
		} else {
			mx = end1.x + ((end2.x - end1.x) / 2);
		}
		if (end1.y > end2.y) {
			my = end2.y + ((end1.y - end2.y) / 2);
		} else {
			my = end1.y + ((end2.y - end1.y) / 2);
		}
		return new PointObj(mx, my);
	}
	calcStrutPoints() {
		this.strutArray = [];
		for (let i = 0; i < this.midPoints.length; i++) {
			let nexti = i + 3;
			if (nexti >= this.midPoints.length) {
				nexti -= this.midPoints.length;
			}
			let thisSP = this.calcProjPoint(this.midPoints[i], this.outerPoints[nexti]);
			this.strutArray[i] = thisSP;
		}
		return this.strutArray;
	}
	calcProjPoint(mp, op) {
		let px, py;
		let adj, opp;
		if (op.x > mp.x) {
			opp = op.x - mp.x;
		} else {
			opp = mp.x - op.x;
		}
		if (op.y > mp.y) {
			adj = op.y - mp.y;
		} else {
			adj = mp.y - op.y;
		}
		if (op.x > mp.x) {
			px = mp.x + (opp * _strutFactor);
		} else {
			px = mp.x - (opp * _strutFactor);
		}
		if (op.y > mp.y) {
			py = mp.y + (adj * _strutFactor);
		} else {
			py = mp.y - (adj * _strutFactor);
		}
		return new PointObj(px, py);
	}
}

実はこのスケッチ、当初p5.jsへの移植がどうしてもうまくいかず、一度諦めてしばらくほったらかしにしていたのでした。今となっては何がダメだったのかよくわからないのですが、その間「Nature of Code」や「The Coding Train」を見ながらいくつか別のスケッチを作り、最近になって改めてチャレンジしたところ、特につまずくことなく動いてしまいました。

以前、あるエンジニアの方から「新しい言語や技術を勉強する時、理解できないところが出てきたら、多少気持ちが悪くても、いったん放って先に進んだほうがいい。しばらくしてから見直すと案外すんなり理解できることが多いから」という話を聞いたことがあったのですが、このスケッチで、まさにそれを体験することになったのでした。

さて「ジェネラティブ・アート」からの移植ものはこれでひととおり出し切りました。今後はオリジナルのスケッチを今まで以上のペースで公開したいなぁと思っとります。